1 入门

前言

HTTP不仅仅服务于web页面,同时也是构建暴露服务和数据的API的强大平台。HTTP有着简单、灵活和无处不在的特点。你能想到的几乎所有平台都包含有一个HTTP库,所以HTTP服务可以遍及广泛的客户端,包括浏览器、移动设备和传统桌面应用程序。

ASP.NET Web API是一个在.NET框架上构建web API的框架。在本教程中,你将使用ASP.NET Web API来创建一个返回产品列表的web API。

创建Web API项目在本教程中,你将使用ASP.NET Web API来创建一个返回产品列表的web API。前端页面使用jQuery来显示结果。

这里写图片描述 图片 1.1 这里写图片描述

开启Visual Studio并在开始页面选择New Project。或者在File目录下选择New,然后选择Project。

在Template面板中,选择Installed Templates,然后展开Visual C#节点。在Visual C#节点下,选择Web。在项目模板列表中,选择ASP.NET Web Application。命名项目为“ProductsApp”并点击OK。

这里写图片描述 图片 1.2 这里写图片描述

在NEW ASP.NET Project对话框中,选择Empty模板。在”Add folders and core references for”,选中Web API。点击OK。

注释:你也可以用“Web API”模板来创建Web API。Web API模板使用了ASP.NET MVC来提供API的帮助页面。我在本教程中使用Empty模板是因为我希望不用MVC来展示Web API。通常,你不必了解ASP.NET MVC就能使用Web API。

添加模型

模型是在你的应用程序中表示数据的对象。ASP.NET Web API能够将你的模型自动序列化成JSON、XML或其他格式,然后将其序列化数据写入到HTTP响应消息的body中。只要客户端能够读取序列化格式,它就能够反序列化出对象。几乎所有客户端都能解析XML或JSON。而且,客户端还能通过在HTTP请求的Accept header中设置来指明它想要的格式。

让我们来创建一个表示产品的简单模型吧。

如果Solution Explorer没有显示出来,点击View菜单,然后选择Solution Explorer。在Solution Explorer中,右击Models文件夹。从上下文菜单中选择Add,然后选择Class。

这里写图片描述 图片 1.3 这里写图片描述

命名该类为“Product”。添加以下属性到Product类中。

  1. namespace ProductsApp.Models
  2. {
  3. public class Product
  4. {
  5. public int Id { get; set; }
  6. public string Name { get; set; }
  7. public string Category { get; set; }
  8. public decimal Price { get; set; }
  9. }
  10. }

添加控制器

在Web API中,控制器(controller)是处理HTTP请求的对象。我们将添加一个能够根据ID来返回多个或一个产品的控制器。

备注:如果你还没有使用过ASP.NET MVC,你应该已经对控制器很熟悉了。Web API的控制器和MVC的控制器很相近,但是它继承的是ApiController而不是Controller。

在Solution Explorer中,右击Controllers文件夹。选择Add,然后选择Controller。

这里写图片描述 图片 1.4 这里写图片描述

在Add Scaffold对话框中,选择Web API Controller – Empty。点击Add。

这里写图片描述 图片 1.5 这里写图片描述

在Add Controller对话框,给控制器命名为”ProductsController”。点击Add。

这里写图片描述 图片 1.6 这里写图片描述

接下来便会在Controllers文件夹下创建一个名为ProductsController.cs的文件。

这里写图片描述 图片 1.7 这里写图片描述

备注:其实你不必非得把控制器添加到Controllers文件夹下。文件夹名称只是为了更方便你组织源文件。

如果文件没有打开,那就双击文件打开它。在文件中替换成以下代码:

  1. using ProductsApp.Models;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Net;
  6. using System.Web.Http;
  7.  
  8. namespace ProductsApp.Controllers
  9. {
  10. public class ProductsController : ApiController
  11. {
  12. Product[] products = new Product[]
  13. {
  14. new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
  15. new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
  16. new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
  17. };
  18.  
  19. public IEnumerable<Product> GetAllProducts()
  20. {
  21. return products;
  22. }
  23.  
  24. public IHttpActionResult GetProduct(int id)
  25. {
  26. var product = products.FirstOrDefault((p) => p.Id == id);
  27. if (product == null)
  28. {
  29. return NotFound();
  30. }
  31. return Ok(product);
  32. }
  33. }
  34. }

为了让示例简单化,products被存储在控制器类中的固定数组中。当然,在实际应用程序中,你可能想要查询数据库或使用其他外部数据源。

控制器定义了两个返回产品的方法: 1. GetAllProducts方法将整个产品列表作为IEnumerable类型返回。 2. GetProduct方法通过它的ID来查找单个产品。

没错,你已经有一个可以使用的web API了。控制器上的每个方法都对应一个或多个URI:

Controlle Method URI
GetAllProducts /api/products
GetProduct /api/products/id

对于GetProduct方法,URI中的id是一个占位符。例如,为了得到一个ID为5的产品,URI是api/products/5。

使用JavaScript和jQuery来调用Web API

在本节中,我们将添加一个使用AJAX来调用Web API的HTML页面。我们将使用jQuery来产生AJAX调用并用返回结果来更新页面。

在Solution Explorer中,右击项目并选择Add,然后选择New Item。

这里写图片描述 图片 1.8 这里写图片描述

在Add New Item对话框中,选择Visual C#下的Web节点,然后选择HTML Page选项。命名页面为“index.html”。

这里写图片描述 图片 1.9 这里写图片描述

用以下代码替换文件中的全部:

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4. <title>Product App</title>
  5. </head>
  6. <body>
  7.  
  8. <div>
  9. <h2>All Products</h2>
  10. <ul id="products" />
  11. </div>
  12. <div>
  13. <h2>Search by ID</h2>
  14. <input type="text" id="prodId" size="5" />
  15. <input type="button" value="Search" onclick="find();" />
  16. <p id="product" />
  17. </div>
  18.  
  19. <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script>
  20. <script>
  21. var uri = 'api/products';
  22.  
  23. $(document).ready(function () {
  24. // Send an AJAX request
  25. $.getJSON(uri)
  26. .done(function (data) {
  27. // On success, 'data' contains a list of products.
  28. $.each(data, function (key, item) {
  29. // Add a list item for the product.
  30. $('<li>', { text: formatItem(item) }).appendTo($('#products'));
  31. });
  32. });
  33. });
  34.  
  35. function formatItem(item) {
  36. return item.Name + ': $' + item.Price;
  37. }
  38.  
  39. function find() {
  40. var id = $('#prodId').val();
  41. $.getJSON(uri + '/' + id)
  42. .done(function (data) {
  43. $('#product').text(formatItem(data));
  44. })
  45. .fail(function (jqXHR, textStatus, err) {
  46. $('#product').text('Error: ' + err);
  47. });
  48. }
  49. </script>
  50. </body>
  51. </html>

有好几个方法去得到jQuery。在本例中,我使用Microsoft Ajax CDN。你也可以在http://jquery.com/下载它,让ASP.NET “Web API”项目包含jQuery。

得到产品列表

为了得到Products列表,可以发送一个HTTP的GET请求到“/api/products”。

jQuery的getJSON函数会发送AJAX请求。其中包含了JSON对象数组。done函数指定了一个当请求成功时触发的回调。在回调中,我们用产品信息更新DOM。

  1. $(document).ready(function () {
  2. // Send an AJAX request
  3. $.getJSON(apiUrl)
  4. .done(function (data) {
  5. // On success, 'data' contains a list of products.
  6. $.each(data, function (key, item) {
  7. // Add a list item for the product.
  8. $('<li>', { text: formatItem(item) }).appendTo($('#products'));
  9. });
  10. });
  11. });

通过ID得到产品

如果想要通过ID来取得产品,可以发送HTTP的GET请求到”/api/products/id“,其中id就是产品的ID。

  1. function find() {
  2. var id = $('#prodId').val();
  3. $.getJSON(apiUrl + '/' + id)
  4. .done(function (data) {
  5. $('#product').text(formatItem(data));
  6. })
  7. .fail(function (jqXHR, textStatus, err) {
  8. $('#product').text('Error: ' + err);
  9. });
  10. }

我们仍然使用getJSON来发送AJAX请求,但是这次我们将ID放到URI请求中。它的响应会是一个代表了单个产品的JSON对象。

运行应用程序按F5开始调试应用程序,web页面看起来会是下面这样:

这里写图片描述 图片 1.10 这里写图片描述

为了通过ID获得产品,输入ID并点击Search。

这里写图片描述 图片 1.11 这里写图片描述

如果你输入了一个无效的ID,那么服务器就会返回HTTP错误消息。

这里写图片描述 图片 1.12 这里写图片描述

使用F12查看HTTP请求和响应

当你工作于HTTP服务时,如果能够查看HTTP请求和响应的详细无疑是非常有帮助的。你可以在IE9中使用F12开发者工具来做这些操作。在IE9中,按F12来打开工具。点击Network面板,并点击Start Capturing。现在返回到web页面,并按F5来重新加载web页面。IE将会捕捉到浏览器和web服务器之间的HTTP传输。下图显示了一个页面的所有HTTP传输。

这里写图片描述 图片 1.13 这里写图片描述

定位到相对URI”api/products/“。选中并点击Go to detailed view。在详细视图中,这里多个面板用于查看请求和响应的header和body。

例如,如果你点击Request headers,你就会看到客户端在Accept header请求了”application/json“。

这里写图片描述 图片 1.14 这里写图片描述

如果你点击了Response body,你就会看到产品列表如何被序列化成JSON。其他浏览器也有相似的功能。另一个有用的工具是Fiddler,它是一个web调试代理工具。你可以使用Fiddler来查看HTTP传输,也可以合成HTTP请求,后者能够给予你在请求上对于HTTP头部的完全控制。

前言

本节的主题是ASP.NET Web API如何将控制器动作的返回值转换成HTTP的响应消息。

Web API控制器动作可以返回下列的任何值: 1, void 2, HttpResponseMessage 3, IHttpActionResult 4, Some other type

取决于返回的以上哪一种,Web API使用不同的机制来创建HTTP响应。

Return type How Web API creates the response
void Return empty 204 (No Content)
HttpResponseMessage Convert directly to an HTTP response message.
IHttpActionResult Call ExecuteAsync to create an HttpResponseMessage, then convert to an HTTP response message.
Other type Write the serialized return value into the response body; return 200 (OK).

本节的剩余部分将详细描述每种返回值。

void

如果返回类型是type,Web API就会用状态码204(No Content)返回一个空HTTP响应。

示例控制器:

  1. public class ValuesController : ApiController
  2. {
  3. public void Post()
  4. {
  5. }
  6. }

HTTP相应:

  1. HTTP/1.1 204 No Content
  2. Server: Microsoft-IIS/8.0
  3. Date: Mon, 27 Jan 2014 02:13:26 GMT

HttpResponseMessage

如果一个动作返回HttpResponseMessage,Web API就通过HttpResponseMessage的属性构造成消息从而直接将返回值转换成HTTP响应。

  1. public class ValuesController : ApiController
  2. {
  3. public HttpResponseMessage Get()
  4. {
  5. HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, "value");
  6. response.Content = new StringContent("hello", Encoding.Unicode);
  7. response.Headers.CacheControl = new CacheControlHeaderValue()
  8. {
  9. MaxAge = TimeSpan.FromMinutes(20)
  10. };
  11. return response;
  12. }
  13. }

相应:

  1. HTTP/1.1 200 OK
  2. Cache-Control: max-age=1200
  3. Content-Length: 10
  4. Content-Type: text/plain; charset=utf-16
  5. Server: Microsoft-IIS/8.0
  6. Date: Mon, 27 Jan 2014 08:53:35 GMT
  7.  
  8. hello

如果传递一个域模型给CreateResponse方法,Web API会使用媒体格式(media formatter)将序列化模型写入到响应体中。

  1. public HttpResponseMessage Get()
  2. {
  3. // Get a list of products from a database.
  4. IEnumerable<Product> products = GetProductsFromDB();
  5.  
  6. // Write the list to the response body.
  7. HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, products);
  8. return response;
  9. }

IHttpActionResult

IHttpActionResult接口在Web API 2中被引进。本质上,它定义了一个HttpResponseMessage工厂。以下是使用IHttpActionResult接口的好处: 1, 简单你的控制器的单元测试 2, 为创建HTTP相应将公共逻辑移动到单独的类 3, 通过隐藏构建相应的底层细节,使控制器动作更清晰

IHttpActionResult包含一个单独的方法ExecuteAsync,它会异步地创建一个HttpResponseMessage实例:

  1. public interface IHttpActionResult
  2. {
  3. Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken);
  4. }

如果一个控制器动作返回IHttpActionResult,Web API会调用ExecuteAsync方法来创建HttpResponseMessage。然后将HttpResponseMessage转换到HTTP相应消息里。

以下是一个IHttpActionResult的简单执行,它创建一个文本相应:

  1. public class TextResult : IHttpActionResult
  2. {
  3. string _value;
  4. HttpRequestMessage _request;
  5.  
  6. public TextResult(string value, HttpRequestMessage request)
  7. {
  8. _value = value;
  9. _request = request;
  10. }
  11. public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
  12. {
  13. var response = new HttpResponseMessage()
  14. {
  15. Content = new StringContent(_value),
  16. RequestMessage = _request
  17. };
  18. return Task.FromResult(response);
  19. }
  20. }

控制器动作示例:

  1. public class ValuesController : ApiController
  2. {
  3. public IHttpActionResult Get()
  4. {
  5. return new TextResult("hello", Request);
  6. }
  7. }

相应:

  1. HTTP/1.1 200 OK
  2. Content-Length: 5
  3. Content-Type: text/plain; charset=utf-8
  4. Server: Microsoft-IIS/8.0
  5. Date: Mon, 27 Jan 2014 08:53:35 GMT
  6.  
  7. hello

更通常的情况是,你会使用System.Web.Http.Results命名空间下定义的IHttpActionResult实现。

在接下来的示例中,如果请求没有匹配到任何已存在的产品ID,控制器就会调用ApiController.NotFound来创建一个404(Not Found)响应。否则,控制器会调用ApiController.OK,它会调用一个意为包含该产品的200(OK)相应。

  1. public IHttpActionResult Get (int id)
  2. {
  3. Product product = _repository.Get (id);
  4. if (product == null)
  5. {
  6. return NotFound(); // Returns a NotFoundResult
  7. }
  8. return Ok(product); // Returns an OkNegotiatedContentResult
  9. }

Other Return Types对于其他所有返回类型,Web API使用媒体格式(media formatter)来序列化返回值。Web API将序列化值写入到响应体中。响应状态码是200(OK)。

  1. public class ProductsController : ApiController
  2. {
  3. public IEnumerable<Product> Get()
  4. {
  5. return GetAllProductsFromDB();
  6. }
  7. }

该实现的缺点在于你不能直接返回一个错误码,比如404。

Web API通过在请求中使用Accept头来选择格式。

示例请求:

  1. GET http://localhost/api/products HTTP/1.1
  2. User-Agent: Fiddler
  3. Host: localhost:24127
  4. Accept: application/json

示例相应:

  1. HTTP/1.1 200 OK
  2. Content-Type: application/json; charset=utf-8
  3. Server: Microsoft-IIS/8.0
  4. Date: Mon, 27 Jan 2014 08:53:35 GMT
  5. Content-Length: 56
  6.  
  7. [{"Id":1,"Name":"Yo-yo","Category":"Toys","Price":6.95}]

第 1 章 前言

在传统的web应用程序中,客户端(浏览器)通过请求页面来启动与服务器的通信。然后服务器处理该请求,并发送HTML页面到客户端。在随后页面上的操作中——例如,用户导航到一个链接或提交一个包含数据的表单——一个新的请求便被发送到服务器,并且重新开始了数据流:服务器处理请求,并将新页面发送到浏览器以响应客户端的新动作请求。

在单页面应用程序(SPA)中,在初始化请求后整个页面在浏览器中被加载出来,但通过Ajax请求来进行后续的交互。这意为着部分页面改变后浏览器仅需要更新,而不需要整个重新加载。SPA实现减少了应用程序对于用户动作的响应时间,从而制造了跟流畅的体验。

在这个动手实验中,你将使用这些先进技术的优势来实现Geek Quiz,这是一个基于SPA概念的网站。你将首先用ASP.NET Web API实现服务层以暴露所需的操作来检索问题和存储答案。然后,你将使用AngularJS和CSS3的变换效果来构建丰富多彩的UI。

第 1 章 回顾

收获

在本动手实验中,你将学习到: 1, 创建ASP.NET Web API服务来发送和接受JSON数据 2, 使用AngularJS来创建响应的UI 3, 使用CSS3变化来加强UI体验

前提工作以下是完成这个动手实验所必须的:

Visual Studio Express 2013 for Web 或更先进的版本

安装为了在这个动手实验中运行这个项目,你需要先安装好开发环境:

1, 打开资源管理器并跳转到lab’s的Source文件夹。 2, 右击Setup.cmd并选择Run as administrator来运行安装程序,这会配置你的开发环境,并为本实验安装Visual Studio代码片(code snippet)。 3, 如果弹出了用户控制窗口,请确认该操作。 备注:在运行安装前请确保你已经检查了本实验所需的所以依赖项。

使用代码片

在实验文档各处,你都被提示去插入代码块。为了更便利,所有这些代码都通过Visual Studio Code Snippets提供,这能够让你在Visual Studio 2013下读取这些代码,而不用手动来做。

备注:每个练习都伴随着该练习Begin文件夹下的开始解决方案,这能让你分别完成每个练习。你要知道这些在练习过程中加入的代码片可能在开始解决方案中被遗失或在你完成练习前无法工作。在每个练习的源码中,你也会发现一个包含了完成相应联系所需的剩余步骤的Visual Studio解决方案的End文件夹。如果你在完成这个动手实验过程中需要额外的帮助,你就可以将这些解决方案当作指导。


第 1 章 练习

该动手实验包含以下练习: 1, Creating a Web API 2, Creating a SPA Interface

预计完成本实验的时间:60分钟

备注:当你第一次启动Visual Studio时,你必须选择一个预定义的设置。每个预定义都被设计成匹配不同的开发风格,并且决定了窗体布局、编辑器行为、智能感知代码片和对话框选项。当使用General Development Settings集合时,实验过程中会给出动作描述以帮助在Visual Studio中完成给定的任务。如果你为你的开发环境选择了不同的设置,那么你该考虑到在接下来的步骤中可能会有差别。

练习1:创建Web API

服务层是SPA的一个关键部分,它能够处理来自UI发送的Ajax请求和在响应调用时返回数据。数据的返回应该是机器可读的格式,这样才能被客户端解析和使用。

Web API框架是ASP.NET栈的一部分,并被设计成更容易地实现HTTP服务,通常是通过RESTful API发送和接收JSON或XML格式的数据。在本练习中,你将创建一个Web站点用以托管Geek Quiz应用程序,然后使用ASP.NET Web API实现后台服务用以暴露和维持知识竞赛(quiz)的数据。

任务1:为Geek Quiz创建初始项目

在本任务中,你将基于Visual Studio的One ASP.NET项目类型来创建一个支持ASP.NET Web API的新的ASP.NET MVC项目。One ASP.NET合并了所有ASP.NET技术,并给予你随意搭配和选择的权利。你将添加Entity Framework的模板类和添加quiz问题的数据库。

  • 打开Visual Stuio Express 2013 for Web,并选择File | New Project… 来创建一个新解决方案。 这里写图片描述 图片 1.15 这里写图片描述
  • 在New Project对话框,选择Visual C# | Web节点下的ASP.NET Web Application。请确保选择了.NET Framework,命名为GeekQuiz,选择一个Location并点击OK。 这里写图片描述 图片 1.16 这里写图片描述
  • 在New ASP.NET Project对话框,选择MVC模板并选择Web API选项。同样请确保Authentication选项被设置到Individual User Account。点击OK以继续。 这里写图片描述 图片 1.17 这里写图片描述
  • 在Solution Explorer,右击GeekQuiz项目的Models文件,并选择Add | Existing Item…

    这里写图片描述 图片 1.18 这里写图片描述

  • 在Add Existing Item对话框,导航到Source/Assets/Model文件夹,并选中所有文件。点击Add。

这里写图片描述 图片 1.19 这里写图片描述

这里写图片描述 图片 1.20 这里写图片描述

备注:通过添加这些文件,你其实就为Geek Quiz应用程序添加了数据模型、Entity Framework数据库上下文和数据库初始化类。

Entity Framework(EF)是一个对象关系映射,它使你能够通过对抽象化的应用程序模型编程来取代关系型存储编程来创建数据库访问。

以下是你刚刚添加的这些类的描述:

  1. TriviaOption: 代表了对应于quiz问题的单个选项
  2. TriviaQues:代表了一个quiz问题和通过Options属性暴露了该问题相关的选项
  3. TriviaAnswer: 代表了对应于一个quiz问题的用户选择的选项
  4. TriviaContext: 代表了Geek Quiz应用程序的Entity Framework数据库上下文。这个类从DContext继承而来并暴露了DbSet属性,后者表示以上所描述的实体对象集合。
  5. TriviaDatabaseInitializer: 通过继承自CreateDatabaseIfNotExistsTriviaContext类实现了Entity Framework初始化功能。该类的默认行为是如果数据库不存在则创建它,以及在Seed方法内插入实体对象。

6 打开Global.asax.cs文件,并添加以下using语句。

  1. using GeekQuiz.Models;

7 在Application_Start方法的开始添加以下代码,它将TriviaDatabaseInitializer设置成数据库初始器。

  1. public class MvcApplication : System.Web.HttpApplication
  2. {
  3. protected void Application_Start()
  4. {
  5. System.Data.Entity.Database.SetInitializer(new TriviaDatabaseInitializer());
  6.  
  7. AreaRegistration.RegisterAllAreas();
  8. GlobalConfiguration.Configure(WebApiConfig.Register);
  9. FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
  10. RouteConfig.RegisterRoutes(RouteTable.Routes);
  11. BundleConfig.RegisterBundles(BundleTable.Bundles);
  12. }
  13. }

8 修改Home控制器以约束对用户的认证。打开Controller文件夹下的HomeController.cs文件,并为HomeController类的定义添加Authorize属性。

  1. namespace GeekQuiz.Controllers
  2. {
  3. [Authorize]
  4. public class HomeController : Controller
  5. {
  6. public ActionResult Index()
  7. {
  8. return View();
  9. }
  10.  
  11. ...
  12. }
  13. }

备注:Authorize过滤器会检查用户是否是经过认证的。如果用户未认证,它会返回HTTP状态码401(Unauthorized)而不会执行用户操作。你可以将过滤器应用成全局的,也可以是控制器级别的,也可以是独立操作级别的。

9 现在你应该来修改web页面布局和绑定。打开Views | Shared文件夹下的_Layout.cshtml文件,通过将My ASP.NET Application修改成Geek Quiz来更新title元素的内容。

  1. <head>
  2. <meta charset="utf-8" />
  3. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  4. <title>@ViewBag.Title - Geek Quiz</title>
  5. @Styles.Render("~/Content/css")
  6. @Scripts.Render("~/bundles/modernizr")
  7.  
  8. </head>

10 在同一个文件中,通过移除About与Contact链接并重命名Home链接为Play来更新导航栏。另外,将Applicaiton name链接也修改成Geek Quiz。导航栏的HTML代码应该看起来是下面这样的:

  1. <div class="navbar navbar-inverse navbar-fixed-top">
  2. <div class="container">
  3. <div class="navbar-header">
  4. <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
  5. <span class="icon-bar"></span>
  6. <span class="icon-bar"></span>
  7. <span class="icon-bar"></span>
  8. </button>
  9. @Html.ActionLink("Geek Quiz", "Index", "Home", null, new { @class = "navbar-brand" })
  10. </div>
  11. <div class="navbar-collapse collapse">
  12. <ul class="nav navbar-nav">
  13. <li>@Html.ActionLink("Play", "Index", "Home")</li>
  14. </ul>
  15. @Html.Partial("_LoginPartial")
  16. </div>
  17. </div>
  18. </div>

11 通过将My ASP.NET Application替换成Geek Quiz以更新页面布局的footer节点。用以下高亮代码来修改

<

footer>元素的内容:

  1. <div class="container body-content">
  2. @RenderBody()
  3. <hr />
  4. <footer>
  5. <p>&copy; @DateTime.Now.Year - Geek Quiz</p>
  6. </footer>
  7. </div>

任务2:创建名为TriviaController的Web API

在前一个任务中,你已经创建了Geek Quiz这个web应用程序的初始结构。现在你要建立一个简单的Web API服务,用以和quiz的数据模型互动并暴露以下动作: GET /api/trivia: 从quiz列表取出已认证用户的下一个问题来让用户回答 POST /api/trivia: 为已认证用户逐一存储quiz答案 你将使用Visual Studio提供的ASP.NET支架工具来为Web API控制器类创建基线。 1 打开App_Start文件夹下的WebApiConfig.cs文件。该文件定义了Web API服务的配置项,例如路由如何映射到Web API控制器的动作。 2 在文件首部添加如下using语句。

  1. using Newtonsoft.Json.Serialization;

3 通过在Register方法内添加如下高亮代码,以全局性配置Web API动作方法返回的JSON数据格式。

  1. public static class WebApiConfig
  2. {
  3. public static void Register(HttpConfiguration config)
  4. {
  5. // Web API configuration and services
  6.  
  7. // Use camel case for JSON data.
  8. config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
  9.  
  10. // Web API routes
  11. config.MapHttpAttributeRoutes();
  12.  
  13. config.Routes.MapHttpRoute(
  14. name: "DefaultApi",
  15. routeTemplate: "api/{controller}/{id}",
  16. defaults: new { id = RouteParameter.Optional }
  17. );
  18. }
  19. }

备注: CamelCasePropertyNamesContractResolver会自动的将属性名称转换成camel式,它是JavaScript中属性的简便名称。 4 在Solution Explorer,右击GeekQuiz项目的Controller文件夹,选择Add | New Scaffolded Item…

这里写图片描述 图片 1.21 这里写图片描述

5 在Add Scaffold对话框,请确保左侧面板的Common节点被选中。然后选择中间面板的Web API 2 Controller – Empty模板并点击Add。

这里写图片描述 图片 1.22 这里写图片描述

备注:ASP.NET Scafolding是一个ASP.NET Web应用程序的代码生成框架。Visual Studio 2013对于MVC和Web API项目包含了预安装的代码生成器。当你想快速添加与数据模型交互的代码以减少开发标准的数据操作所需的时间,你就可以在项目中使用支架(scaffolding)。

支架(scaffolding)同样会确保项目中的依赖项都被安装。例如,如果你从一个空的ASP.NET项目开始,然后使用支架来添加了Web API控制器,与之相关的Web API NuGet包和引用会被自动添加你的项目中。

6 在Add Controller对话框,在Controller name文本框中键入TriviaController并点击Add。

这里写图片描述 图片 1.23 这里写图片描述

7 之后TriviaController.cs文件就会被添加到GeekQuiz项目的Controller文件夹下,并包含一个空的TriviaController类。然后在文件开头添加以下using语句。

  1. using System.Data.Entity;
  2. using System.Threading;
  3. using System.Threading.Tasks;
  4. using System.Web.Http.Description;
  5. using GeekQuiz.Models;

8 在TriviaController类开头添加以下代码,在控制器中定义、初始化和配置TriviaContext实例。

  1. public class TriviaController : ApiController
  2. {
  3. private TriviaContext db = new TriviaContext();
  4.  
  5. protected override void Dispose(bool disposing)
  6. {
  7. if (disposing)
  8. {
  9. this.db.Dispose();
  10. }
  11.  
  12. base.Dispose(disposing);
  13. }
  14. }

备注:TriviaController的Dispose方法执行TriviaContext实例的Dispose方法,它会确保当TriviaContext实例被创建或垃圾回收时,所有被上下文对象使用的资源都被释放掉。这也包括了被Entity Framework打开的数据库连接。

9 在TrivialController类的结尾加上以下helper方法。这个方法根据具体用户从数据库中取出quiz问题给用户回答。

  1. private async Task<TriviaQuestion> NextQuestionAsync(string userId)
  2. {
  3. var lastQuestionId = await this.db.TriviaAnswers
  4. .Where(a => a.UserId == userId)
  5. .GroupBy(a => a.QuestionId)
  6. .Select(g => new { QuestionId = g.Key, Count = g.Count() })
  7. .OrderByDescending(q => new { q.Count, QuestionId = q.QuestionId })
  8. .Select(q => q.QuestionId)
  9. .FirstOrDefaultAsync();
  10.  
  11. var questionsCount = await this.db.TriviaQuestions.CountAsync();
  12.  
  13. var nextQuestionId = (lastQuestionId % questionsCount) + 1;
  14. return await this.db.TriviaQuestions.FindAsync(CancellationToken.None, nextQuestionId);
  15. }

10 在TrivialController类中添加以下Get动作的方法。这个动作的方法调用了在上一步中定义的用于为认证用户取出下一个问题的NextQuestionAsync方法。

  1. // GET api/Trivia
  2. [ResponseType(typeof(TriviaQuestion))]
  3. public async Task<IHttpActionResult> Get()
  4. {
  5. var userId = User.Identity.Name;
  6.  
  7. TriviaQuestion nextQuestion = await this.NextQuestionAsync(userId);
  8.  
  9. if (nextQuestion == null)
  10. {
  11. return this.NotFound();
  12. }
  13.  
  14. return this.Ok(nextQuestion);
  15. }

11 在TriviaController类中添加以下helper方法。

  1. private async Task<bool> StoreAsync(TriviaAnswer answer)
  2. {
  3. this.db.TriviaAnswers.Add(answer);
  4.  
  5. await this.db.SaveChangesAsync();
  6. var selectedOption = await this.db.TriviaOptions.FirstOrDefaultAsync(o => o.Id == answer.OptionId
  7. && o.QuestionId == answer.QuestionId);
  8.  
  9. return selectedOption.IsCorrect;
  10. }

12 在TriviaController类中添加以下Post动作方法。这个动作方法将答案和认证用户联系起来,并调用StoreAsync方法。然后,它会根据帮助方法的返回值发送一个包含布尔变量的响应。

  1. // POST api/Trivia
  2. [ResponseType(typeof(TriviaAnswer))]
  3. public async Task<IHttpActionResult> Post(TriviaAnswer answer)
  4. {
  5. if (!ModelState.IsValid)
  6. {
  7. return this.BadRequest(this.ModelState);
  8. }
  9.  
  10. answer.UserId = User.Identity.Name;
  11.  
  12. var isCorrect = await this.StoreAsync(answer);
  13. return this.Ok<bool>(isCorrect);
  14. }

13 通过向TriviaContorller的类定义添加Authorize属性,来修改Web API控制器以约束对认证用户的访问。

  1. [Authorize]
  2. public class TriviaController : ApiController
  3. {
  4. ...
  5. }

任务3:运行解决方案

在本任务中,你将核实你在上一个任务重建立的Web API服务是否如预期般工作。你将会使用Internet Explorer的F12 Developer Tools来捕获网络数据传输和检测来着Web API服务的全部响应。

备注:确保Visual Studio工具栏的Start按钮上被选中为Internet Explorer。

1 按F5以运行解决方案。Log in页面应该会出现在浏览器上。 备注:当应用程序启动时,默认的MVC路由被触发,它会默认地映射到HomeController类的Index动作上。因为HomeController被约束为认证用户(在练习1中你已经将Authorize属性添加到该类上),并且现在还没有用户经过认证,所以应用程序会向登录页面发送原始请求。

这里写图片描述 图片 1.24 这里写图片描述

2 点击Register来创建一个新用户。

这里写图片描述 图片 1.25 这里写图片描述

3 在Register页面,输入一个用户名和密码,然后点击Register。

这里写图片描述 图片 1.26 这里写图片描述

4 接下来应用程序就会注册一个新账号,并且该账号是经过认证的,随后会自动导航到主页。

这里写图片描述 图片 1.27 这里写图片描述

5 在浏览器上按F12以打开Developer Tools面板。按Ctrl+4或点击Network图标,然后点击绿色箭头按钮以开始捕获网络传输。

这里写图片描述 图片 1.28 这里写图片描述

6 在浏览器地址栏的URL上添加api/trivia。你将会检测到来自TriviaController里的Get动作的响应细节。

这里写图片描述 图片 1.29 这里写图片描述

备注:一旦下载完成,你将会被提示对下载的文件进行操作。留着这个对话框,为了在Developers Tool窗口查看响应的内容。 7 现在你可以检测到响应体了。点击Details,然后点击Response body。你能发现下载的数据是一个对应着TriviaQuestion类的options(它是一个TriviaOption对象的列表)、id和title属性的对象。

这里写图片描述 图片 1.30 这里写图片描述

8 返回到Visual Studio并按Shift+F5以停止调试。

练习2:创建SPA界面

在本练习中,你将首先创建Geek Quiz的web前端,使用AngularJS专注于单页面应用程序的交互。然后你将使用CSS3来执行丰富的动画和提供一个当从一个问题转换到另一个问题时切换上下文的可视化效果以加强用户体验。

任务1:使用AngularJS来创建SPA界面

在本任务中,你将使用AngularJS来实现Geek Quiz应用程序的客户端。AngularJS是一个开源的JavaScript框架,它能够搭配MVC以加强基于浏览器的应用程序,使其对于开发和测试都更加便利。

你将从在Visual Studio的Package Manager Console来安装AngularJS开始。然后,你将创建一个控制器用以提供Geek Quiz应用的行为和使用AngularJS模板引擎来提交quiz问题和答案的视图。

备注:关于AngularJS的更多信息,请查看http://angularjs.org/。 1. 打开Visual Studio Express 2013 for Web并打开位于Source/Ext2-CreatingASPAInterface/Begin文件夹下的GeekQuiz.sln解决方案。或者你也可以继续上一个练习所保留的解决方案。 2. 打开Tools | Library Package Manager下的Package Manager Console,键入以下命令以安装AngularJS.Core的NuGet包。 Install-Package AngularJS.Core 3. 在Solution Explorer,右击GeekQuiz项目下的Scripts文件夹,并选择Add | New Folder。为文件夹命名为app并按Enter。 4. 右击你刚刚创建的app文件夹,并选择Add | JavaScript File。

这里写图片描述 图片 1.31 这里写图片描述

  • 在Specify Name for Item对话框,在Iten name文本框中键入quiz-controller并点击OK。

这里写图片描述 图片 1.32 这里写图片描述

  • 在quiz-controller.js文件中,添加以下代码以声明和初始化AngularJS的QuizCtrl控制器。
  1. angular.module('QuizApp', [])
  2. .controller('QuizCtrl', function ($scope, $http) {
  3. $scope.answered = false;
  4. $scope.title = "loading question...";
  5. $scope.options = [];
  6. $scope.correctAnswer = false;
  7. $scope.working = false;
  8.  
  9. $scope.answer = function () {
  10. return $scope.correctAnswer ? 'correct' : 'incorrect';
  11. };
  12. });

备注:QuizCtrl的构造函数期望的参数命名为$scope。scope的初始状态应该是在构造函数中被设置,通过将属性添加到$scope对象。该属性包含了视图模型,并且当控制器被注册时可以访问到模板。

QuizCtrl控制器在一个名为QuizApp的模块内定义。模块是让你能够将应用程序分成独立组件的工作单元。使用模块的主要优势是代码更加易于理解、方便单元测试、可重复使用和可维护。 7. 现在你将给scope添加行为以响应来自视图的事件触发。添加以下代码到QuizCtrl控制器的底部,它在$scope对象中定义了nextQuestion函数。

  1. .controller('QuizCtrl', function ($scope, $http) {
  2. ...
  3.  
  4. $scope.nextQuestion = function () {
  5. $scope.working = true;
  6. $scope.answered = false;
  7. $scope.title = "loading question...";
  8. $scope.options = [];
  9.  
  10. $http.get("/api/trivia").success(function (data, status, headers, config) {
  11. $scope.options = data.options;
  12. $scope.title = data.title;
  13. $scope.answered = false;
  14. $scope.working = false;
  15. }).error(function (data, status, headers, config) {
  16. $scope.title = "Oops... something went wrong";
  17. $scope.working = false;
  18. });
  19. };
  20. };

备注:该函数从上一个练习中创建的Web API Trivia中取出下一个问题,并将问题的数据附加到$scope对象上。 8. 添加以下代码到QuizCtrl控制器的底部,它在$scope对象中定义了sendAnswer函数。

  1. .controller('QuizCtrl', function ($scope, $http) {
  2. ...
  3.  
  4. $scope.sendAnswer = function (option) {
  5. $scope.working = true;
  6. $scope.answered = true;
  7.  
  8. $http.post('/api/trivia', { 'questionId': option.questionId, 'optionId': option.id }).success(function (data, status, headers, config) {
  9. $scope.correctAnswer = (data === true);
  10. $scope.working = false;
  11. }).error(function (data, status, headers, config) {
  12. $scope.title = "Oops... something went wrong";
  13. $scope.working = false;
  14. });
  15. };
  16. };

备注:这个函数发送了根据用户选择的答案到Trivia Web API,并存储该结果——例如,该结果是否正确——到$scope对象中。

  • 下一步是创建AngularJS模板用以定义quiz的视图。打开Views | Home文件夹下的Index.cshtml文件,并替换文件内容为以下代码:

  1. @{
  2. ViewBag.Title = "Play";
  3. }

  4. <div id="bodyContainer" ng-app="QuizApp">

  5. <section id="content">

  6. <div class="container" >

  7. <div class="row">

  8. <div class="flip-container text-center col-md-12" ng-controller="QuizCtrl" ng-init="nextQuestion()">

  9. <div class="back" ng-class="{flip: answered, correct: correctAnswer, incorrect:!correctAnswer}">

  10. <p class="lead">{{answer()}}</p>

  11. <p>

  12. <button class="btn btn-info btn-lg next option" ng-click="nextQuestion()" ng-disabled="working">Next Question</button>

  13. </p>

  14. </div>

  15. <div class="front" ng-class="{flip: answered}">

  16. <p class="lead">{{title}}</p>

  17. <div class="row text-center">

  18. <button class="btn btn-info btn-lg option" ng-repeat="option in options" ng-click="sendAnswer(option)" ng-disabled="working">{{option.title}}</button>

  19. </div>

  20. </div>

  21. </div>

  22. </div>

  23. </div>

  24. </section>

  25. </div>

  26. @section scripts {

  27. @Scripts.Render("~/Scripts/angular.js")

  28. @Scripts.Render("~/Scripts/app/quiz-controller.js")

  29. }

备注:AngularJS模板是声明性的规范,使用模型和控制器的信息将静态标记在用户可见的浏览器中转换成动态视图。以下是模板中用到的AngularJS元素和元素属性的示例: 1, ng-app指令告诉AngularJS表示应用程序根元素的DOM元素 2, ng-controller指令在指令声明的位置将控制器添加到DOM上 3, 花括号标记{{}}表明绑定到在控制器中定义的scope属性 4, ng-click指令被用于响应用户点击并执行定义在scope中的函数

  • 打开Content文件夹下的Site.css文件,并添加以下高亮的样式到文件底部,它提供了一个不错的quiz视图。
  1. .validation-summary-valid {
  2. display: none;
  3. }
  4.  
  5. /* Geek Quiz styles */
  6. .flip-container .back,
  7. .flip-container .front {
  8. border: 5px solid #00bcf2;
  9. padding-bottom: 30px;
  10. padding-top: 30px;
  11. }
  12.  
  13. #content {
  14.  
  15. position:relative;
  16. background:#fff;
  17. padding:50px 0 0 0;
  18. }
  19.  
  20. .option {
  21. width:140px;
  22. margin: 5px;
  23. }
  24.  
  25. div.correct p {
  26. color: green;
  27. }
  28.  
  29. div.incorrect p {
  30. color: red;
  31. }
  32.  
  33. .btn {
  34. border-radius: 0;
  35. }
  36.  
  37. .flip-container div.front, .flip-container div.back.flip {
  38. display: block;
  39. }
  40.  
  41. .flip-container div.front.flip, .flip-container div.back {
  42. display: none;
  43. }

任务2:运行解决方案

在本任务中,你将执行你用AngularJS创建的新的用户界面来回答一些quiz问题。

1 按F5来运行解决方案 2 注册一个新账户。重做练习1中任务3的注册步骤。 备注:如果你使用的是上一个练习所保留的解决方案,那你就可以使用此前注册的账户。 3 Home页面应该会出现,并显示第一个quiz问题。通过点击其中一个选项以回答问题。这将会触发此前定义的sendAnswer函数,它会发送选中的选项到Trivia Web API。

这里写图片描述 图片 1.33 这里写图片描述

4 再点击其中一个按钮之后,结果就会出现。点击Next Question来显示接下来的问题。这将会触发在控制器中定义的nextQuestion函数。

这里写图片描述 图片 1.34 这里写图片描述

5 下一个问题应该会出现。尽可能地继续多回答几个问题。在完成所有的问题后,你应该会返回到第一个问题。

这里写图片描述 图片 1.35 这里写图片描述

6 返回Visual Studio并按Shift+F5来停止调试。 任务3:使用CSS3创建翻转动画

在本任务重,你将使用CSS3执行丰富的动画,通过在一个问题被回答和下一个问题被提取出来时添加一个翻转效果。

1 在Solution Explorer中,右击GeekQuiz项目下的Content文件夹,并选择Add | Existing Item。

这里写图片描述 图片 1.36 这里写图片描述

2 在Add Existing Item对话框,导航到Source/Assets文件夹,并选择Flip.css。点击OK。

这里写图片描述 图片 1.37 这里写图片描述

3 打开你刚刚添加的Flip.css并检查其内容。 4 跳转到flip transformation注释,以下样式使用CSS的perspective和rotateY变换来生成了一个“卡片翻转”效果。

  1. /* flip transformation */
  2. .flip-container div.front {
  3. -moz-transform: perspective(2000px) rotateY(0deg);
  4. -webkit-transform: perspective(2000px) rotateY(0deg);
  5. -o-transform: perspective(2000px) rotateY(0deg);
  6. transform: perspective(2000px) rotateY(0deg);
  7. }
  8.  
  9. .flip-container div.front.flip {
  10. -moz-transform: perspective(2000px) rotateY(179.9deg);
  11. -webkit-transform: perspective(2000px) rotateY(179.9deg);
  12. -o-transform: perspective(2000px) rotateY(179.9deg);
  13. transform: perspective(2000px) rotateY(179.9deg);
  14. }
  15.  
  16. .flip-container div.back {
  17. -moz-transform: perspective(2000px) rotateY(-180deg);
  18. -webkit-transform: perspective(2000px) rotateY(-180deg);
  19. -o-transform: perspective(2000px) rotateY(-180deg);
  20. transform: perspective(2000px) rotateY(-180deg);
  21. }
  22.  
  23. .flip-container div.back.flip {
  24. -moz-transform: perspective(2000px) rotateY(0deg);
  25. -webkit-transform: perspective(2000px) rotateY(0deg);
  26. -ms-transform: perspective(2000px) rotateY(0);
  27. -o-transform: perspective(2000px) rotateY(0);
  28. transform: perspective(2000px) rotateY(0);
  29. }

5 跳转到hide back of pane during flip注释。

  1. /* hide back of pane during flip */
  2. .front, .back {
  3. -moz-backface-visibility: hidden;
  4. -webkit-backface-visibility: hidden;
  5. backface-visibility: hidden;
  6. }

6 打开App_Start文件夹下的BundleConfig.cs文件,并添加引用到Flip.css。

  1. bundles.Add(new StyleBundle("~/Content/css").Include(
  2. "~/Content/bootstrap.css",
  3. "~/Content/site.css",
  4. "~/Content/Flip.css"));

7 按F5以运行解决方案并登录。 8 通过点击任一选项来回答问题。注意两个视图间的翻转效果。

这里写图片描述 图片 1.38 这里写图片描述

9 点击Next Question以提取下一个问题。翻转效果也会再次出现。

这里写图片描述 图片 1.39 这里写图片描述

第 1 章 总结

通过完成这个动手实验室,你已经学会了: 1, 使用ASP.NET Scaffolding来创建ASP.NET Web API控制器 2, 实现Web API的Get动作以提取下一个quiz问题 3, 实现Web API的Post动作以存储quiz答案 4, 在Visual Studio的Package Manger Console安装AngularJS 5, 实现AngularJS模板和控制器 6, 使用CSS3变换来执行动作效果