- 3 处理数据
- 前言
- 创建项目
- 配置Azure设置(可选)
- 前言
- 添加模型类
- 添加Web API 控制器在这部分,我们将添加支持CRUD(create, read, update 和 delete)的Web API 控制器。这些控制器使用Entity Framework来同数据库层交流。
- 前言
- 探索API(可选)
- 查看数据库(可选)当你执行了Update-Database命令,EF会创建数据库并调用Seed方法。当你在本地执行了应用程序后,EF会使用LocalDB。你可以在Visual Studio中查看数据库。在View目录下,选择SQL Server Object Explorer。
- 前言
- 预加载和延迟加载
- Eager Loading(预加载)
- Lazy Loading(延迟加载)
- 显式加载(Explicit Loading)
- 导航属性和环形引用(Navigation Properties and Circular References)当我定义Book和Author模型时,我在Book类中为Book-Author关系定义了导航属性,但我没有在其他方向定义导航属性。
- 添加Knockout库
- 创建视图模型
- 添加Script Bundle
- 部署到新站点
3 处理数据
前言
本指南将会教你使用ASP.NET Web API作后端创建web应用程序的基本技能。本指南使用Entity Framework 6作为数据层,使用knockout.js作为客户端的JavaScript应用程序。本指南也会展示部署应用到Azure App service Web Apps。
本指南使用搭配Entity Framework 6的ASP.NET Web API 2来创建一个操作后端数据库的web应用程序。这是一个你将创建的应用程序截图。
图片 3.1 这里写图片描述
这个应用使用single-page application (SPA) 设计。“Single-page application”是一个通过加载HTML页面然后动态更新页面以取代加载新页面的web应用程序的统称。在初始化页面加载后,应用通过AJAX请求和服务器交流。应用通过AJAX请求返回的JSON数据来更新UI。
AJAX不新颖,但今天这里使用了JavaScript框架,它使得建立一个大而精密的SPA应用程序更加容易。本教程使用了Knockout.js,但你可以使用任何JavaScript客户端框架。
以下是这个应用程序的主要构造块: 1, ASP.NET MVC 创建HTML页面。 2, ASP.NET Web API 处理AJAX请求并返回JSON数据。 3, Knockout.js (数据)绑定HTML元素到JSON数据。 4, Entity Framework 和数据库交流。
创建项目
打开Visual Studio。在File目录下,选择New,然后选择Project。(或在开始页面点击New Project。)
在New Project对话框中,点击左面板的Web和中间面板的ASP.NET Web Application。给项目命名为BookService并点击OK。
图片 3.2 这里写图片描述
在New ASP.NET Project对话框中,选择Web API容器。
图片 3.3 这里写图片描述
如果你希望将项目托管在Azure App Service,请使Host in the cloud被选中。
配置Azure设置(可选)
如果你保留Host in cloud选项被选中,Visual Studio就会指引你去登陆Microsoft Azure。
图片 3.4 这里写图片描述
在你登录到Azure后,Visual Studio还会让你去配置web应用。为站点输入名称,选择你的Azure订阅,并选择国家和地区。在Database server下,选择Create new server。输入管理员用户名和密码。
图片 3.5 这里写图片描述
前言
在本部分中,你将添加用于定义数据库实体的模型类。然后你将添加用于在这些实体上执行CRUD(Create、Retrieve、Update、Delete——译者注)操作的Web API 控制器。
添加模型类
在本教程中,我们将通过使用“Code First”的方法对实体框架(EF)来创建数据库。对于Code First,你写C#类来相应数据库表,使用EF来创建数据库。(有关详细信息,见Entity Framework Development Approaches.)
首先,我们定义我们的域对象作为POCO。我们将创建以下POCO: Author Book
在解决方案资源管理中,右击Models文件夹。选择Add,然后选择Class。名为这个类为Author。
图片 3.6 这里写图片描述
用以下代码替换Author.cs中的所有样板代码。
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- namespace BookService.Models
- {
- public class Author
- {
- public int Id { get; set; }
- [Required]
- public string Name { get; set; }
- }
- }
添加另一个命名为Book的类,并替换成以下代码。
- using System.ComponentModel.DataAnnotations;
- namespace BookService.Models
- {
- public class Book
- {
- public int Id { get; set; }
- [Required]
- public string Title { get; set; }
- public int Year { get; set; }
- public decimal Price { get; set; }
- public string Genre { get; set; }
- // Foreign Key
- public int AuthorId { get; set; }
- // Navigation property
- public Author Author { get; set; }
- }
- }
Entity Framework将使用这些模型来创建数据库表。对于每一个模型,Id属性将变成数据库表的主键列。
在Book类中,AuthorId定义了一个外键到Author表。(简单起见,我假定每本书只有一个作者。)该book类还包含一个导航属性给相关的Author。 你可以使用导航属性在代码中访问相关的作者。我在【Web API系列教程】3.4 — 实战:处理数据(处理实体关系) 中描述了关于导航属性的更多信息。
添加Web API 控制器在这部分,我们将添加支持CRUD(create, read, update 和 delete)的Web API 控制器。这些控制器使用Entity Framework来同数据库层交流。
首先,你应该删除Controllers目录下的ValuesControllers.cs文件。这个文件包含了一个Web API示例,但对于本教程你并不需要它。
图片 3.7 这里写图片描述
然后,编译这个项目。Web API框架使用反射来发现这个模型类,所以它需要编译程序集。
在Solution Explorer中,右击Controllers文件夹。选择Add,然后选择Controller。
图片 3.8 这里写图片描述
在Add Scaffold对话框中,选择“Web API 2 controller with actions, using Entity Framework”。点击Add。
图片 3.9 这里写图片描述
在Add Controller对话框中,执行以下操作: 1, 在模型类下拉框中,选择Author类。(如果你没有在下拉框中看到它,请确保已经编译了这个项目。) 2, 选中“Use async controller action”。 3, 保留控制器名称为“AuthorsController”。 4, 点击加号(+)按钮下一步到Data Context Class.
图片 3.10 这里写图片描述
在New Data Context对话框中,保留默认名称并点击Add。
图片 3.11 这里写图片描述
点击Add以完成Add Controller对话框。这个对话将添加两个类到你的项目中: AuthorsController定义了一个Web API控制器。这个控制器实现了REST API,客户端使用它来在authors列表上执行CRUD操作。 BookServiceContext在运行时管理实体对象,包括从数据库中聚集对象数据、追踪、保留数据到数据库。它继承自DBContext。
图片 3.12 这里写图片描述
在这个节点上,再次编译这个项目。现在再过一遍相同的步骤为Book实体添加API控制器。这次选择Book作为模型类,并选择已经存在的BookServiceContext类作为数据上下文类。(不要再创建新的数据上下文。)点击Add以添加该控制器。
图片 3.13 这里写图片描述
前言
在本部分中,你将在EF上使用Code First Migration来用测试数据建立数据库。
在Tools目录下选择Library Package Manager,然后选择Package Manager Console。在包管理控制台窗口,输入以下命令:
- Enable-Migrations
这条命令会添加一个名为Migrations的文件夹到你的项目,并添加一个名为Configuration.cs的代码文件到Migrations文件夹。
图片 3.14 这里写图片描述
如果在BookService中出现多种上下文类型,请输入”Enable-Migrations –ContextTypeName BookService.Models.BookServiceContext”,具体请看下图。——译者注。
图片 3.15 这里写图片描述
打开Configuration.cs文件。添加以下using语句。
- using BookService.Models;
然后添加以下代码到Configuration.Seed方法:
- protected override void Seed(BookService.Models.BookServiceContext context)
- {
- context.Authors.AddOrUpdate(x => x.Id,
- new Author() { Id = 1, Name = "Jane Austen" },
- new Author() { Id = 2, Name = "Charles Dickens" },
- new Author() { Id = 3, Name = "Miguel de Cervantes" }
- );
- context.Books.AddOrUpdate(x => x.Id,
- new Book() { Id = 1, Title = "Pride and Prejudice", Year = 1813, AuthorId = 1,
- Price = 9.99M, Genre = "Comedy of manners" },
- new Book() { Id = 2, Title = "Northanger Abbey", Year = 1817, AuthorId = 1,
- Price = 12.95M, Genre = "Gothic parody" },
- new Book() { Id = 3, Title = "David Copperfield", Year = 1850, AuthorId = 2,
- Price = 15, Genre = "Bildungsroman" },
- new Book() { Id = 4, Title = "Don Quixote", Year = 1617, AuthorId = 3,
- Price = 8.95M, Genre = "Picaresque" }
- );
- }
在Package Manager Console窗口,键入以下命令:
- Add-Migration Initial
- Update-Database
第一条命令生成用于创建数据库的代码,第二条命令执行那些代码。数据库使用LocalDB并创建于本地。
图片 3.16 这里写图片描述
探索API(可选)
按F5在debug模式下运行应用程序。Visual Studio启动IIS Express并运行你的web应用。Visual Studio会启动一个浏览器并打开app的主页。
当Visual Studio运行了这个web项目,它会给定一个端口号。在下图中,端口号是50524。当你运行应用程序的时候,你可能会看到不同的端口号。
图片 3.17 这里写图片描述
主页使用ASP.NET MVC来实现。在页面顶部有一个写着“API”的链接。该链接会带你去一个自动生成的关于Web API的帮助页面。(想了解这个帮助页面如何生成的,以及你怎样添加你自己的文档进该页面,查看Creating Help Pages for ASP.NET Web API(http://www.asp.net/web-api/overview/creating-web-apis/creating-api-help-pages)。)你可以点击帮助页面的链接以查看API的详细信息,包括请求和相应的格式。
图片 3.18 这里写图片描述
该API支持在数据库上执行CRUD操作。下表是关于API的总结。
Authors | 备注 |
---|---|
GET api/authors | Get all authors. |
GET api/authors/{id} | Get an author by ID. |
POST /api/authors | Create a new author. |
PUT /api/authors/{id} | Update an existing author. |
DELETE /api/authors/{id} | Delete an author. |
Books | 备注 |
---|---|
GET /api/books | Get all books. |
GET /api/books/{id} | Get a book by ID. |
POST /api/books | Create a new book. |
PUT /api/books/{id} | Update an existing book. |
DELETE /api/books/{id} | Delete a book. |
查看数据库(可选)当你执行了Update-Database命令,EF会创建数据库并调用Seed方法。当你在本地执行了应用程序后,EF会使用LocalDB。你可以在Visual Studio中查看数据库。在View目录下,选择SQL Server Object Explorer。
图片 3.19 这里写图片描述
在Connect to Server对话框中,在Server Name编辑框,键入“(localdb)\v11.0”。保留Authentication选项为”Windows Authentication”。点击Connect。
图片 3.20 这里写图片描述
Visual Studio会连接到LocalDB并在SQL Server Object Explorer窗口显示已经存在的数据库。你可以展开该节点查看EF创建的表。
图片 3.21 这里写图片描述
为了看到数据,右击一个表并选择View Data。
图片 3.22 这里写图片描述
下面的截图显示了Books表的结果。注意EF通过seed数据聚集了数据库,并且该表包含了指向Authors表的外键。
图片 3.23 这里写图片描述
前言
本部分描述了EF如何加载相关实体的细节,并且如何在你的模型类中处理环形导航属性。(本部分预备了背景知识,而这不是完成这个教程所必须的。你也可以跳到第五节)
预加载和延迟加载
预加载和延迟加载的英文名称分别是Eager Loading和Lazy Loading。
当EF与关系数据库一同使用时,了解EF是如何加载相关数据是非常重要的。
去查看EF生成的SQL查询也是很有帮助的。为了追踪SQL,添加下列代码到BookServiceContext构造器中:
- public BookServiceContext() : base("name=BookServiceContext")
- {
- // New code:
- this.Database.Log = s => System.Diagnostics.Debug.WriteLine(s);
- }
如果发送一个GET请求到/api/books,它返回像下面这样的JSON:
- [
- {
- "BookId": 1,
- "Title": "Pride and Prejudice",
- "Year": 1813,
- "Price": 9.99,
- "Genre": "Comedy of manners",
- "AuthorId": 1,
- "Author": null
- },
- ...
你能看到Author属性是空的,即便book包含有效的AuthorId。那是因为EF没有在加载相关的Author实体。关于SQL查询的跟踪日志如下:
- SELECT
- [Extent1].[BookId] AS [BookId],
- [Extent1].[Title] AS [Title],
- [Extent1].[Year] AS [Year],
- [Extent1].[Price] AS [Price],
- [Extent1].[Genre] AS [Genre],
- [Extent1].[AuthorId] AS [AuthorId]
- FROM [dbo].[Books] AS [Extent1]
该SQL跟踪在Visual Studio的Output窗口中显示。——译者注
SELECT语句从Books表中获取数据,但并没有引用Author表。 作为参考,这里是在BooksController类中的方法,它返回books的列表。
- public IQueryable<Book> GetBooks()
- {
- return db.Books;
- }
来看看我们如何才能让Author作为返回的JSON数据的一部分。在Entity Framework中有三种方式加载相关数据:预加载(eager loading)、延迟加载(lazy loading)和显式加载(explicit loading)。我们应该在这三种技术中有所取舍,所以了解它们是如何工作的就非常重要了。
Eager Loading(预加载)
在预加载中,EF加载相关数据作为初始化数据库查询的一部分。为了执行预加载,使用System.Data.Entity.Include扩展方法。
- public IQueryable<Book> GetBooks()
- {
- return db.Books
- // new code:
- .Include(b => b.Author);
- }
这会告诉EF将Author数据包含在查询中。如果你做了这个改变并运行了app,现在JSON数据会是如下所示:
- [
- {
- "BookId": 1,
- "Title": "Pride and Prejudice",
- "Year": 1813,
- "Price": 9.99,
- "Genre": "Comedy of manners",
- "AuthorId": 1,
- "Author": {
- "AuthorId": 1,
- "Name": "Jane Austen"
- }
- },
- ...
其跟踪日志显示EF在Book和Author表中执行了一个join操作。
- SELECT
- [Extent1].[BookId] AS [BookId],
- [Extent1].[Title] AS [Title],
- [Extent1].[Year] AS [Year],
- [Extent1].[Price] AS [Price],
- [Extent1].[Genre] AS [Genre],
- [Extent1].[AuthorId] AS [AuthorId],
- [Extent2].[AuthorId] AS [AuthorId1],
- [Extent2].[Name] AS [Name]
- FROM [dbo].[Books] AS [Extent1]
- INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[AuthorId]
Lazy Loading(延迟加载)
在延迟加载中,当实体的导航属性是非关联时,EF会自动加载一个相关的实体。为了使用延迟加载,使导航属性变成虚拟的。例如,在Book类中:
- public class Book
- {
- // (Other properties)
- // Virtual navigation property
- public virtual Author Author { get; set; }
- }
现在考虑如下代码:
- var books = db.Books.ToList(); // Does not load authors
- var author = books[0].Author; // Loads the author for books[0]
当延迟加载开启时,在books[0]上访问Author属性会使EF为author查询数据库。
延迟加载需要多段数据库操作过程,因为每次EF发送一个查询它都会取出一次相关实体。通常,你希望为序列化的对象禁用延迟加载。序列化已经在模型上读取了所有可能触发加载相关实体的属性。例如,下面是当延迟加载开启后EF序列化books列表时的SQL查询。你可以看到EF对于三个作者做了三次不同的查询。
- SELECT
- [Extent1].[BookId] AS [BookId],
- [Extent1].[Title] AS [Title],
- [Extent1].[Year] AS [Year],
- [Extent1].[Price] AS [Price],
- [Extent1].[Genre] AS [Genre],
- [Extent1].[AuthorId] AS [AuthorId]
- FROM [dbo].[Books] AS [Extent1]
- SELECT
- [Extent1].[AuthorId] AS [AuthorId],
- [Extent1].[Name] AS [Name]
- FROM [dbo].[Authors] AS [Extent1]
- WHERE [Extent1].[AuthorId] = @EntityKeyValue1
- SELECT
- [Extent1].[AuthorId] AS [AuthorId],
- [Extent1].[Name] AS [Name]
- FROM [dbo].[Authors] AS [Extent1]
- WHERE [Extent1].[AuthorId] = @EntityKeyValue1
- SELECT
- [Extent1].[AuthorId] AS [AuthorId],
- [Extent1].[Name] AS [Name]
- FROM [dbo].[Authors] AS [Extent1]
- WHERE [Extent1].[AuthorId] = @EntityKeyValue1
但还有很多时候你可能想要使用延迟加载。预加载会造成EF生成非常复杂的联接。或者你可能需要对于小的数据集合的相关实体,延迟加载会更加有效。
避免序列化问题的一种方式是序列化数据传输对象(DTOs)而不是实体对象。我将会在后面的文章中展示这种实现。
显式加载(Explicit Loading)
显式加载和延迟加载非常类似,除了你在代码中显式地获取相关数据;当你访问导航属性时它不会自动发生。显示加载会在加载相关数据时给你更多的控制权,但也需要额外的代码。关于显示加载的更多信息,请查看Loading Related Entities。 http://msdn.microsoft.com/en-us/data/jj574232#explicit
导航属性和环形引用(Navigation Properties and Circular References)当我定义Book和Author模型时,我在Book类中为Book-Author关系定义了导航属性,但我没有在其他方向定义导航属性。
如果你在Author类中也定义相应的导航属性会怎样呢?
- public class Author
- {
- public int AuthorId { get; set; }
- [Required]
- public string Name { get; set; }
- public ICollection<Book> Books { get; set; }
- }
不幸的是,当你在序列化模型时这会产生一个问题。如果你加载相关数据,它会产生环形对象图。
图片 3.24 这里写图片描述
当JSON或XML格式试图序列化图时,它将会抛出一个异常。这两个格式抛出不同异常信息。这里是JSON格式的示例:
- {
- "Message": "An error has occurred.",
- "ExceptionMessage": "The 'ObjectContent`1' type failed to serialize the response body for content type
- 'application/json; charset=utf-8'.",
- "ExceptionType": "System.InvalidOperationException",
- "StackTrace": null,
- "InnerException": {
- "Message": "An error has occurred.",
- "ExceptionMessage": "Self referencing loop detected with type 'BookService.Models.Book'.
- Path '[0].Author.Books'.",
- "ExceptionType": "Newtonsoft.Json.JsonSerializationException",
- "StackTrace": "...”
- }
- }
这里是XML格式的示例:
- <Error>
- <Message>An error has occurred.</Message>
- <ExceptionMessage>The 'ObjectContent`1' type failed to serialize the response body for content type
- 'application/xml; charset=utf-8'.</ExceptionMessage>
- <ExceptionType>System.InvalidOperationException</ExceptionType>
- <StackTrace />
- <InnerException>
- <Message>An error has occurred.</Message>
- <ExceptionMessage>Object graph for type 'BookService.Models.Author' contains cycles and cannot be
- serialized if reference tracking is disabled.</ExceptionMessage>
- <ExceptionType>System.Runtime.Serialization.SerializationException</ExceptionType>
- <StackTrace> ... </StackTrace>
- </InnerException>
- </Error>
一个解决方案是使用DTO,我将会在下一节中描述它。你可以配置JSON或XML格式化程序来处理图循环。关于更多信息,请查看Handling Circular Object References. (http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization#handling_circular_object_references)
对于本教程,你不需要Author.Book导航熟悉,所以你可以去掉它。
现在,我们的Web API暴露数据库实体给客户端,而客户端接收直接映射到你的数据库表的数据。然而,这不永远都是个好办法。有时候你可以想要改变发送到客户端的数据的形式。例如,你可以想要: 1, 移除环形引用(见上一章) 2, 隐藏客户端不应该看到的特定属性 3, 为了减少有效载荷而省略一些属性 4, 拼接包含嵌套的对象图,以使它们对客户端更便利 5, 避免”over-posting”漏洞(查看Model Validation(http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api)关于over-posting的讨论) 6, 对你的服务层和数据层进行解耦
为了完成它,你应该定义一个数据传输对象(DTO,data transfer object)。DTO是一个定义了数据将会如何在网络上传输的对象。让我们来看看它如何工作于Book实体。在Models文件夹下,添加两个DTO类:
- namespace BookService.Models
- {
- public class BookDTO
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public string AuthorName { get; set; }
- }
- }
- namespace BookService.Models
- {
- public class BookDetailDTO
- {
- public int Id { get; set; }
- public string Title { get; set; }
- public int Year { get; set; }
- public decimal Price { get; set; }
- public string AuthorName { get; set; }
- public string Genre { get; set; }
- }
- }
BookDetailDTO类包含了Book模型的所有属性,除了用于承载作者姓名的字符串AuthorName。BookDTO类包含了BookDetailDTO的属性的子集。
下一步,在BooksController类中替换两个返回DTO的GET方法。我们将使用LINQ的Select语句将Book实体转换到DTO。
- // GET api/Books
- public IQueryable<BookDTO> GetBooks()
- {
- var books = from b in db.Books
- select new BookDTO()
- {
- Id = b.Id,
- Title = b.Title,
- AuthorName = b.Author.Name
- };
- return books;
- }
- // GET api/Books/5
- [ResponseType(typeof(BookDetailDTO))]
- public async Task<IHttpActionResult> GetBook(int id)
- {
- var book = await db.Books.Include(b => b.Author).Select(b =>
- new BookDetailDTO()
- {
- Id = b.Id,
- Title = b.Title,
- Year = b.Year,
- Price = b.Price,
- AuthorName = b.Author.Name,
- Genre = b.Genre
- }).SingleOrDefaultAsync(b => b.Id == id);
- if (book == null)
- {
- return NotFound();
- }
- return Ok(book);
- }
这是新的GetBooks方法所生成的SQL查询。你可以查看EF从LINQ Select转换到SQL Select的语句。
- SELECT
- [Extent1].[Id] AS [Id],
- [Extent1].[Title] AS [Title],
- [Extent2].[Name] AS [Name]
- FROM [dbo].[Books] AS [Extent1]
- INNER JOIN [dbo].[Authors] AS [Extent2] ON [Extent1].[AuthorId] = [Extent2].[Id]
最后,修改PostBook方法以返回DTO。
- [ResponseType(typeof(Book))]
- public async Task<IHttpActionResult> PostBook(Book book)
- {
- if (!ModelState.IsValid)
- {
- return BadRequest(ModelState);
- }
- db.Books.Add(book);
- await db.SaveChangesAsync();
- // New code:
- // Load author name
- db.Entry(book).Reference(x => x.Author).Load();
- var dto = new BookDTO()
- {
- Id = book.Id,
- Title = book.Title,
- AuthorName = book.Author.Name
- };
- return CreatedAtRoute("DefaultApi", new { id = book.Id }, dto);
- }
总结:在这个教程中,我们在代码中手动地转换到DTO。另一种方式是使用像AutoMapper这样的库来处理自动转换。
在本节,你将使用HTML、JavaScript和Knockout.js库为应用程序创建客户端。我们将按如下步骤建立客户端应用: 1, 展示books列表 2, 展示book详细信息 3, 添加一本新书
Knockout.js库使用了模型-视图-视图模型(MVVM)模式: 1, 模型是在业务域(在本例中是books和authors)中数据在服务器端的表现形式。 2, 视图是表示层(HTML)。 3, 视图模型是维持模型的JavaScript对象。视图模型是UI的代码抽象。它不具备HTML表现形式,相反,它表示抽象特征的视图,例如”书籍列表“。
视图被数据绑定到视图模型。视图模型的更新将自动反映在视图中。视图模型也从视图中获取事件,比如按钮的点击。
图片 3.25 这里写图片描述
这个实现使得在你的app中修改布局和UI更加容易,因为你可以改变这些绑定而无须任何代码。例如,你可能以ul的方式展示一个项列表,那么可以将其改变成表。
添加Knockout库
在Visual Studio中,点击Tools目录,选择Library Package Manager。然后选择Package Manager Console。在Package Manager Console窗口,输入以下命令:
- Install-Package knockoutjs
这条命令将Knockout文件添加到Scripts文件夹下。
创建视图模型
在Scripts文件夹下添加一个名为app.js的JavaScript文件。(在Solution Explorer中,右击Scripts文件夹,选择Add,然后选择JavaScript File。)粘贴以下代码:
- var ViewModel = function () {
- var self = this;
- self.books = ko.observableArray();
- self.error = ko.observable();
- var booksUri = '/api/books/';
- function ajaxHelper(uri, method, data) {
- self.error(''); // Clear error message
- return $.ajax({
- type: method,
- url: uri,
- dataType: 'json',
- contentType: 'application/json',
- data: data ? JSON.stringify(data) : null
- }).fail(function (jqXHR, textStatus, errorThrown) {
- self.error(errorThrown);
- });
- }
- function getAllBooks() {
- ajaxHelper(booksUri, 'GET').done(function (data) {
- self.books(data);
- });
- }
- // Fetch the initial data.
- getAllBooks();
- };
- ko.applyBindings(new ViewModel());
在Knockout中,observable类启用了数据绑定。当observable的内容改变时,observable会通知所有的数据绑定控制器,所以它们能够去更新它们自身。(而observable类是一个observable的数组版本。)以此开始,我们的视图模型有了两个observable: 1, books维持books列表。 2, error包含如果AJAX调用失败时的错误信息
该getAllbooks方法产生AJAX调用以得到books列表。然后将其结果添加到books数组中。
而ko.applyBinding方法是Knockout库的一部分。它使用视图模型作为一个参数并建立数据绑定。
添加Script Bundle
Bundle是一个在ASP.NET 4.5中出现的新特性,它使得组合或包装多个文件进一个文件更加容易。Bundle减少了向服务器的请求,而这恰能改进页面加载时间。
打开App_Start文件夹下的BundleConfig.cs文件,添加如下代码到RegisterBundles方法。
- public static void RegisterBundles(BundleCollection bundles)
- {
- // ...
- // New code:
- bundles.Add(new ScriptBundle("~/bundles/app").Include(
- "~/Scripts/knockout-{version}.js",
- "~/Scripts/app.js"));
- }
在本节,你将开始为app定义HTML,并在HTML和视图模型间添加数据绑定。
打开Views/Home/Index.cshtml文件。用以下代码替换掉文件的所有内容。
- @section scripts {
- @Scripts.Render("~/bundles/app")
- }
<div class="page-header">
<h1>BookService</h1>
</div>
<div class="row">
<div class="col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h2 class="panel-title">Books</h2>
</div>
<div class="panel-body">
<ul class="list-unstyled" data-bind="foreach: books">
<li>
<strong><span data-bind="text: AuthorName"></span></strong>: <span data-bind="text: Title"></span>
<small><a href="#">Details</a></small>
</li>
</ul>
</div>
</div>
<div class="alert alert-danger" data-bind="visible: error"><p data-bind="text: error"></p></div>
</div>
<div class="col-md-4">
<!— TODO: Book details —>
</div>
<div class="col-md-4">
<!— TODO: Add new book —>
</div>
</div>
这里的绝大部分div元素都是Bootstrap风格,它是有着数据绑定属性的重要元素。这个元素将HTML链接到视图模型中。
例如:
- <p data-bind="text: error">
在这个示例中,“text”绑定会使得p元素显示在视图模型中error属性的值。error的回调被声明在ko.obserable中:
- self.error = ko.observable();
无论何时一个新值被修改到error上,Knockout都会在< p >元素上更新该文本。
而foreach绑定告诉Knockout在books数组中循环遍历。对于该数组的每一项,Knockout会创建一个新的< li >元素。绑定foreach引用的上下文到数组的每一项。例如:
- <span data-bind="text: Author"></span>
这里有一个text绑定读取每一个book的Author属性。
如果你现在运行该应用程序,它看起来会是这样:
图片 3.26 这里写图片描述
在页面加载后,books列表会被异步的加载。现在,“Details”链接还不具备功能。我们将在下一节为其添加此功能。
在本节,你将添加查看每本书的详细信息的功能。在app.js中,添加以下代码到视图模型:
- self.detail = ko.observable();
- self.getBookDetail = function (item) {
- ajaxHelper(booksUri + item.Id, 'GET').done(function (data) {
- self.detail(data);
- });
- }
在Views/Home/Index.cshtml,添加一个数据绑定元素到Details链接:
- <ul class="list-unstyled" data-bind="foreach: books">
- <li>
- <strong><span data-bind="text: AuthorName"></span></strong>: <span data-bind="text: Title"></span>
- <!-- New code -->
- <small><a href="#" data-bind="click: $parent.getBookDetail">Details</a></small>
- </li>
- </ul>
它为< a >元素绑定了一个在视图模型中调用getBookDetail函数的点击事件。
在同一个文件,替换以下代码:
- <div class="col-md-4">
- <!-- TODO: Book details -->
- </div>
到:
- <!-- ko if:detail() -->
- <div class="col-md-4">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h2 class="panel-title">Detail</h2>
- </div>
- <table class="table">
- <tr><td>Author</td><td data-bind="text: detail().AuthorName"></td></tr>
- <tr><td>Title</td><td data-bind="text: detail().Title"></td></tr>
- <tr><td>Year</td><td data-bind="text: detail().Year"></td></tr>
- <tr><td>Genre</td><td data-bind="text: detail().Genre"></td></tr>
- <tr><td>Price</td><td data-bind="text: detail().Price"></td></tr>
- </table>
- </div>
- </div>
- <!-- /ko -->
这段代码创建了一个绑定到视图模型中details各个属性的表。
其中的“<!—ko —>”句法让你在DOM元素外部包括一个Knockout绑定。在本例中,if绑定导致本节的标记要显示详细信息时才为非空。
- <!-- ko if:detail() -->
现在如果你运行这个应用程序,并点击其中一个“Detail“链接,这个app会展示出book的详细信息。
图片 3.27 这里写图片描述
在本节,你将添加让用户可以创建新book的功能。在app.js中,添加如下代码到视图模型:
- self.authors = ko.observableArray();
- self.newBook = {
- Author: ko.observable(),
- Genre: ko.observable(),
- Price: ko.observable(),
- Title: ko.observable(),
- Year: ko.observable()
- }
- var authorsUri = '/api/authors/';
- function getAuthors() {
- ajaxHelper(authorsUri, 'GET').done(function (data) {
- self.authors(data);
- });
- }
- self.addBook = function (formElement) {
- var book = {
- AuthorId: self.newBook.Author().Id,
- Genre: self.newBook.Genre(),
- Price: self.newBook.Price(),
- Title: self.newBook.Title(),
- Year: self.newBook.Year()
- };
- ajaxHelper(booksUri, 'POST', book).done(function (item) {
- self.books.push(item);
- });
- }
- getAuthors();
在Index.cshtml中,替换以下代码:
- <div class="col-md-4">
- <!-- TODO: Add new book -->
- </div>
到:
- <div class="col-md-4">
- <div class="panel panel-default">
- <div class="panel-heading">
- <h2 class="panel-title">Add Book</h2>
- </div>
- <div class="panel-body">
- <form class="form-horizontal" data-bind="submit: addBook">
- <div class="form-group">
- <label for="inputAuthor" class="col-sm-2 control-label">Author</label>
- <div class="col-sm-10">
- <select data-bind="options:authors, optionsText: 'Name', value: newBook.Author"></select>
- </div>
- </div>
- <div class="form-group" data-bind="with: newBook">
- <label for="inputTitle" class="col-sm-2 control-label">Title</label>
- <div class="col-sm-10">
- <input type="text" class="form-control" id="inputTitle" data-bind="value:Title"/>
- </div>
- <label for="inputYear" class="col-sm-2 control-label">Year</label>
- <div class="col-sm-10">
- <input type="number" class="form-control" id="inputYear" data-bind="value:Year"/>
- </div>
- <label for="inputGenre" class="col-sm-2 control-label">Genre</label>
- <div class="col-sm-10">
- <input type="text" class="form-control" id="inputGenre" data-bind="value:Genre"/>
- </div>
- <label for="inputPrice" class="col-sm-2 control-label">Price</label>
- <div class="col-sm-10">
- <input type="number" step="any" class="form-control" id="inputPrice" data-bind="value:Price"/>
- </div>
- </div>
- <button type="submit" class="btn btn-default">Submit</button>
- </form>
- </div>
- </div>
- </div>
这段代码创建了一个表单,用于提交新的作者。作者下拉框的值被数据绑定到视图模型的authors中。对于其他的表单输入,这些值都被数据绑定到视图模型的newBook属性。
这个表单上的提交事件被数据绑定到addBook函数:
- <form class="form-horizontal" data-bind="submit: addBook">
这个addBook函数读取数据绑定表单输入中的当前值,并创建JSON对象。然后会POST这个JSON对象到/api/books。
在这最后一节中,你将把应用程序发布到Azure。在Solution Explorer中,右击项目并选择Publish。
图片 3.28 这里写图片描述
点击Publish打开Publish Web对话框。如果你在新建项目的时候选中了Host in Cloud,那么链接和设置就已经都配置好了。在这种情况下,仅仅是需要点击Settings面板,然后选择“Execute Code First Migrations”。(如果你没有在开始的时候选中”Host in Cloud”,那么请跟随如下步骤)(http://www.asp.net/web-api/overview/data/using-web-api-with-entity-framework/part-10#new-website)
图片 3.29 这里写图片描述
为了部署应用,点击Publish。你可以在Web Publish Activity窗口查看发布过程。(在View目录下,选择Other Windows,然后选择Web Publish Activity。)
图片 3.30 这里写图片描述
当Visual Studio完成app的部署,默认的浏览器会自动打开部署网站的URL,你所创建的应用程序现在就运行在云端了。浏览器地址栏中的URL显示了站点以及被从网络中加载出来了。
图片 3.31 这里写图片描述
部署到新站点
如果你没有在建立项目的时候选中Host in Cloud,那么你现在需要配置一个新的web应用。在Solution Explorer,右击项目,并选择Publish。选择Profile面板,并点击Microsoft Azure Websites。如果你现在没有登录到Azure,你需要先去登陆。
图片 3.32 这里写图片描述
在Existing Websites对话框,点击New。
图片 3.33 这里写图片描述
输入站点名称。选择你的Azure订阅和地区。在Database server下,选择Create New Server,或已经存在的server。点击Create。
图片 3.34 这里写图片描述
点击Settings面板,然后选择“Execute Code First Migrations”。然后点击Publish。