chsakell分享了前端使用AngularJS,後端使用ASP.NET Web API的購物車案例,很是精彩,這裏這裏記錄下對此項目的理解。javascript
文章:
http://chsakell.com/2015/01/31/angularjs-feat-web-api/
http://chsakell.com/2015/03/07/angularjs-feat-web-api-enable-session-state/html
源碼:
https://github.com/chsakell/webapiangularjssecurity前端
本系列共三篇,本篇是第二篇。java
購物車Demo,前端使用AngularJS,後端使用ASP.NET Web API(1)--後端
購物車Demo,前端使用AngularJS,後端使用ASP.NET Web API(2)--前端,以及先後端Session
購物車Demo,前端使用AngularJS,後端使用ASP.NET Web API(3)--Idetity,OWIN先後端驗證git
HomeController用來展現主頁面,並接受前端傳來的Order的編號。angularjs
public calss HomeCOntroller : Controller { public ActionReuslt Index() { retun View(); } public ActionResult ViewOrder(int id) { using(var context = new SotreContext()) { //這時候Order的導航屬性Gadgets尚未加載出來呢 var order = context.Orders.Find(id); //根據Order編號獲取中間表 var gadgetOrders = context.GadgetOrders.Where(go => go.OrderID == id); foreach(GadgetOrder gadgetOrder in gadgetOrders) { //加載中間表某個記錄中對應的導航屬性 context.Entry(gadgetOrder).Reference(g => g.Gadget).Load(); order.Gadgets.Add(gadgetOrder.Gadget); } return View(order); } } }
Home/Index.cshtml視圖。github
<html ng-app="gadgetsStore"> ... <body ng-controller='gadgetStoreCtrl'> <div ng-hide="checkoutComplete()"> <div ng-show="showFilter()"> <form> <input type="text" ng-model="searchItem"> </form> </div> <cart-details></cart-details> </div> <div ng-show="data.error" ng-cloak> {{data.error.status}} </div> <ng-view /> <script src="../../Scripts/angular.js" type="text/javascript"></script> <script src="../../Scripts/angular-route.js" type="text/javascript"></script> <script src="../../app/mainApp.js"></script> <script src="../../app/controllers/gadgetsStore.js" type="text/javascript"></script> <script src="../../app/filters/storeFilters.js" type="text/javascript"></script> <script src="../../app/controllers/gadgetsControllers.js" type="text/javascript"></script> <script src="../../app/components/cartCmp.js" type="text/javascript"></script> <script src="../../app/controllers/checkoutController.js" type="text/javascript"></ </body> </html>
以上,ng-hide="checkoutComplete()"決定着是否顯示所在div,ng-show="data.error" 決定是否顯示報錯,<ng-view />根據路由顯示不一樣視圖,ng-cloak用來避免在切換視圖時頁面的閃爍,<cart-details></cart-details>是自定義的directive,和angularjs有關的js文件放在頂部,applicaiton相關js文件放在其下面,在mainApp.js文件中坐落着一個頂級module名稱是gadgetStore,而頂級controller被放在了gadgetsStoreCtrl.js這個js文件中了。web
最終的界面以下:後端
main.js 聲明頂級module,以及配置路由。api
angular.module("gadgetsStore", ["storeFilters", "storeCart", "ngRoute"]) .config(function($routeProvider){ $routeProvider.when("/gadgets",{templateUrl: "app/views/gadgets.html"}); $routeProvider.when("/checkout",{templateUrl: "app/views/checkout.html"}); $routeProvider.when("/submitorder",{templateUrl: "app/views/submitOrder.html"}); $routeProvider.when("/complete",{templateUrl: "app/views/orderSubmitted.html"}); $routeProvider.otherwise({templateUrl: "app/views/gadgets.html"}); });
storeFilters, storeCart是咱們自定義的,這裏注入進來。
有了gadgetsStore這個module,如今就爲這個module添加controller等。
angular.module('gadgetsStore') .constant('gadgetsUrl', 'http://localhost:8888/api/gadgets') .constant('ordersUrl', 'http://localhost:8888/api/orders') .constant('categoreisUrl', 'http://localhost:8888/api/categories') .controller('gadgetStore', function($scope, $http, $location, gadgetsUrl, categoresUrl, ordersUrl, cart){//由於gadgetsStore依賴引用了storeCart,因此這裏能夠引用cart //這裏的data被用在主視圖上,因此data的數據會被其它部分視圖共享 // $scope.data.gadgets // scope.data.erro // $scope.data.categories // $scope.data.OrderLocation // $scope.data.OrderID // $scope.data.orderError $scope.data = {}; $http.get(gadgetsUrl) .success(function(data){ $scope.data.gadgets = data; }) .error(function(error){ $scope.data.error = error; }); $http.get(categoresUrl) .success(function(data){ $scope.data.categories = data; }) .error(function(error){ $scope.data.error = error; }); $scope.sendOrder = function(shippingDetails){ var order = angular.copy(shippingDetails); order.gadgets = cart.getProducts(); $http.post(ordersUrl, order) .success(function(data, status, headers, config){ $scope.data.OrderLocation = headers('Location'); $scope.data.OrderID = data.OrderID; cart.getProducts().length = 0; }) .error(function(error){ $scope.data.orderError = error; }).finally(function(){ $location.path("/complete"); }); } $scope.showFilter = function(){ return $location.path() == ''; } $scope.checkoutComplete = function(){ return $location.path() == '/complete'; } });
以上,爲gadgetsStore這個module定義了常量以及controller。把一些規定的uri定義成某個moudule的常量是很好的習慣。經過$location.path方法能夠獲取或設置當前窗口的uri。
好了,頂級的module和頂級的controller有了,Gadget部分如何顯示呢?
根據路由$routeProvider.when("/gadgets",{templateUrl: "app/views/gadgets.html"}), Gadget的視圖被放在了app/views/gadgets.html中了,來看gadgets.html這個視圖。
<div ng-controller="gadgetsCtrl" ng-hide="data.error"> <!--左側導航部分--> <div> <!--這裏的selectCategory方法實際是把controller內部的一個變量selectedCategory設爲null--> <a ng-click="selectCategory()">Home</a> <a ng-repeat="item in data.categoires | orderBy: 'CategoryID'" ng-click="selectCategory(item.CategoryID)" ng-class="getCategoryClass(item.CategoryID)">{{item.Name}}</a> </div> <!--右側Gadgets部分--> <div> <div ng-repeat="item in data.gadgets | filter: categoryFilterFn | filter: searchItem | range:selectedPage:pageSize"> {{item.Name}} {{item.Price | currency}} <img ng-src="../../images/{{item.Images}}" /> {{item.Description}} <a ng-click="addProductToCart(item)">Add To Cart</a> </div> <!--分頁部分--> <div> <a ng-repeat="page in data.gadgets | filter:categoryFilterFn | filter:searchItem | pageCount:pageSize" ng-click="selectPage($index + 1)" ng-class="getPageClass($index + 1)"> {{$index + 1}} </a> </div> </div> </div>
以上,把視圖的來源交給了gadgetsCtrl這個controller, 這個controller也被定義在了gadgetsStore這個module中。
gadgetsCtr.js
angular.module("gadgetsStore") .constant("gadgetsActiveClass", 'btn-primary') .constant('gadgetsPageCount', 3) .controller("gadgetsCtrl", function($scope, $filter, gadgetsActiveClass, gadgetsPageCount, cart){ //存儲Category的主鍵CategoryID var selectedCategory = null; //這裏是傳給range和pageCount過濾器的 $scope.selectedPage = 1; $scope.pageSise = gadgetsPageCount; //實際就是未selectedPage這個變量賦新值 $scope.selectPage = function(newPage){ $scope.selectedPage = newPage; } //這裏把Category的編號CategoryID傳了進來 $scope.selecteCategory = function(newCategory) { $selectedCategory = newCategory; $scope.selectedPage = 1; } //這裏的product實際就是Gadget //過濾出Gadget的CategoryID和這裏的selectedCateogory一致的那些Gadgets $scope.categoryFilterFn = fucntion(product){ return selectedCategory == null || product.CategoryID == selectedCategory; } //category實際是Category的主鍵CategoryID $scope.getCategoryClass = function(category){ return selectedCategory == category ? gadgetsActiveClass : ""; } $scope.getPageClass = function(page){ return $scope.selectedPage = page ? gadgetsActiveClass : ""; } $scope.addProductToCart = function(product){ cart.addProduct(product.GadgetID, product.Name, product.Price, product.CategoryID); } });
在顯示Gadget列表的時候,<div ng-repeat="item in data.gadgets | filter: categoryFilterFn | filter: searchItem | range:selectedPage:pageSize">,這裏用到了一個自定的過濾器range,這個過濾器被定義在了storeFilters.js中。
var storeFilters = angular.module('storeFilters',[]); storeFitlers.filter("range", function($filter){ return function(data, page, size){ if(angular.isArray(data) && angular.isNumber(page) && angular.isNumber(size)){ var start_index = (page - 1)*size; if(data.legnth < start_index){ return []; } else { return $filter("limitTo")(data.splice(start_index), size); } } else{ return data; } } }); sortFilters.filter("pageCount", function(){ return function(data, size){ if(angular.isArray(data)) { var result = []; for(var i = 0; i < Math.ceil(data.length/size); i++){ result.push(i); } } else { return data; } } });
再來看$routeProvider.when("/checkout",{templateUrl: "app/views/checkout.html"});這個路由,checkout.html這個部分視圖以下:
<div ng-controller = "cartDetailsController"> <div ng-show="cartData.length==0"> no item in the shopping cart </div> <div ng-hide="cartData.length == 0"> {{item.count}} {{item.Name}} {{item.Price | currency}} {{(item.Price * item.count) | currency}} <button ng-click="remove(item.GadgetID)"></button> {{total() | currency}} <a href="#">Continue shopping</a> <a href="#/submitorder">Place order now</a> </div> </div>
對應的界面以下:
cartDetailsController這個controller也被放在了頂級module裏。以下:
angular.module("gadgetsStore") .controller("cartDetailsController", function($scope, cart){ $scope.cartData = cart.getProducts(); $scope.total = function(){ var total = 0; for(var i = 0; i < $scope.cartData.length;i++) { total += ($scope.cartData[i].Price * $scope.cartData[i].count); } return total; } $scope.remove = function(id){ cart.removeProduct(id); } });
咱們注意到,咱們已經在多個地方注入cart這個服務 ,這個自定義的服務能夠以factory的方式來建立,若是要用這個cart服務,它所在的module就要被其它module所引用。下面來建立cart服務:
var storeCart = angular.module('storeCart',[]); storeCart.factory('cart', function(){ var cartData = []; return { addProduct: function(id, name, price, category){ //用來標記是否已經向購物車裏加了產品 var addedToExistingItem = false; for(var i=0; i < cartData.length;i++) { if(cartData[i].GadgetID == id){ cartData[i].count++; addedToExistingItem = true; break; } } if(!addedToExistingItem) { cartData.push({ count:1, GadgetID: id, Price: price, Name: name, CategoryID:category }); } }, removeProduct: function(id){ for(var i = 0; i < cartData.legnth; i++){ if(cartData[i].GadgetID == id){ cartData.splice(i, 1); break; } } }, getProducts:function(){ return cartData; } }; });
關於購物車部分,咱們還記得,在主視圖用了<cart-details></cart-details>這個自定義的directive,實際也是在storeCart這個module中定義的。
sortCart.directive("cartDetails", function(cart){ return { restrict: "E", templateUrl: "app/components.cartDetails.html", controller: function($scope){ var cartData = cart.getProducts(); $scope.total = function(){ var total =0; for(var i = 0; i < cartData.legnth; i++){ total += (cartData[i].Price * cartData[i].count); } return total; } $scope.itemCount = function(){ var total = 0; for(var i = 0; i < cartData.length; i++){ total += cartData[i].count; } return total; } } }; });
以上,對應的視圖爲:
Your cart: {{itemCount()}} items {total() | currency} <a href="#/checkout">Checkout</a>
在顯示購物車明細的時候,給出了提交訂單的連接:
<a href="#/submitorder">Place order now</a>
根據路由$routeProvider.when("/submitorder",{templateUrl: "app/views/submitOrder.html"}),是會加載app/views/submitOrder.html部分視圖,界面以下:
對應的html爲:
<form name="shippingForm" novalidate> <input name="companyName" ng-model="data.shipping.CompanyName" required /> <span ng-show="shippingForm.companyName.$error.required"></span> <input name="name" ng-model="data.shipping.OwnerName" required /> <span ng-show="shippingorm.name.$error.required"></span> ... <button ng-disabled="shippingForm.$invalid" ng-click="sendOrder(data.shipping)">Complete Order</button> </form>
sendOrder被定義在了頂級module中:
$scope.sendOrder = function (shippingDetails) { var order = angular.copy(shippingDetails); order.gadgets = cart.getProducts(); $http.post(ordersUrl, order) .success(function (data, status, headers, config) { $scope.data.OrderLocation = headers('Location'); $scope.data.OrderID = data.OrderID; cart.getProducts().length = 0; }) .error(function (error) { $scope.data.orderError = error; }).finally(function () { $location.path("/complete"); }); }
/complete會路由到$routeProvider.when("/complete",{templateUrl: "app/views/orderSubmitted.html"}), app/views/orderSubmitted.html部分視圖以下:
其html部分爲:
<div ng-show="data.orderError"> {{data.orderError.status}}the order could not be placed, <a href="#/submitorder">click here to try again</a> </div> <div ng-hide="data.orderError"> {{data.OrderID}} <a href="#">Back to gadgets</a> <a href="{{data.OrderLocation}}">View Order</a> </div>
■ 實現購物車的Session
如今爲止,還存在的問題是:當刷新頁面的時候,購物車內的產品就會消失,即還麼有Session機制。
與ASP.NET Web API路由相關的HttpControllerRouteHandler, HttpControllerHandler, IRequireSessionState。
首先一個繼承內置的HttpControllerHandler,並實現內置的IRequiresSessionState接口。
public class SessionEnabledControllerHandler : HttpControllerHandler, IRequiresSessionState { public SessionEnabledControllerHandler(RouteData routeData) : base(routeData) { } }
而後實現一個內置HttpControllerRouteHandler的繼承類。
public class SessionEnabledHttpControllerRouteHandler : HttpControllerRouteHandler { protected override IHttpHandler GetHttpHandler(RequestContext requestContext) { return new SessionEnabledControllerHandler(requestContext.RouteData); } }
註釋掉WebApiConfig.cs中的代碼:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.MapHttpAttributeRoutes(); // Moved to RouteConfig.cs to enable Session /* config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); */ } }
在RouteConfig中配置以下:
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); #region Web API Routes // Web API Session Enabled Route Configurations routes.MapHttpRoute( name: "SessionsRoute", routeTemplate: "api/sessions/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ).RouteHandler = new SessionEnabledHttpControllerRouteHandler(); ; // Web API Stateless Route Configurations routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); #endregion #region MVC Routes routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); #endregion } }
以上,須要引用System.Web.Http。
如今,若是但願ItemsController中使用Session,那就這樣請求:
http://localhost:61691/api/sessions/items
若是不想用Session,那就這樣請求:
http://localhost:61691/api/items
如今,在前端,向購物車添加產品相關代碼爲:
addProduct: function (id, name, price, category) { var addedToExistingItem = false; for (var i = 0; i < cartData.length; i++) { if (cartData[i].GadgetID == id) { cartData[i].count++; addedToExistingItem = true; break; } } if (!addedToExistingItem) { cartData.push({ count: 1, GadgetID: id, Price: price, Name: name, CategoryID: category }); } }
相似地,建立一個模型:
public class CartItem { public int Count { get; set; } public int GadgetID { get; set; } public decimal Price { get; set; } public string Name { get; set; } public int CategoryID { get; set; } }
對應的控制器爲:
public class TempOrdersController : ApiController { //get api/TempOrders public List<CartItem> GetTempOrders() { List<CartItem> cartItems = null; if(System.Web.HttpContext.Current.Session["Cart"] != null){ cartItems = (List<CartItem>)System.Web.HttpContext.Current.Session["Cart"]; } return cartItems; } //post api/TempOrders [HttpPost] public HttpResponseMessage SaveOrder(List<CarItem> cartItems) { if (!ModelState.IsValid) { return new HttpResponseMessage(HttpStatusCode.BadRequest); } System.Web.HttpContext.Current.Session["Cart"] = cartItems; return new HttpResponseMessage(HttpStatusCode.OK); } }
再回到前端,首先在gadgetsStore這個頂級module中增長有關緩存API的uri常量。
angular.module('gadgetsStore') .constant('gadgetsUrl', 'http://localhost:61691/api/gadgets') .constant('ordersUrl', 'http://localhost:61691/api/orders') .constant('categoriesUrl', 'http://localhost:61691/api/categories') .constant('tempOrdersUrl', 'http://localhost:61691/api/sessions/temporders') .controller('gadgetStoreCtrl', function ($scope, $http, $location, gadgetsUrl, categoriesUrl, ordersUrl, tempOrdersUrl, cart) { // Code omitted
從新定義cart這個服務:
storeCart.factory('cart', function(){ var cartData = []; return { addProduct: function(id, name, price, category){ var addedToExistingItem = false; for(var i = 0; i < cartData.length; i++){ if(cartData[i].GadgetID == id){ cartData[i].count++; addedToExistingItem = true; break; } } if(!addedToExistingItem){ cartData.push({ count:1, GadgetID: id, Price: price, Name: name, Category: category }); } }, removeProduct: fucntion(id){ for(var i = 0; i < cartData.length; i++){ if(cartData[i].GadgetID == id){ cartData.splice(i, 1); break; } } }, getProducts: fucntion(){ return cartData; }, pushItem: function(item){ cartData.push({ count: item.Count, GadgetID:item.GadgetID, Price: Item.Price, Name: item.Name, CategoryID: item.CategoryID }) } }; });
爲了在頁面每次刷新的時候保證Session的狀態,在主module中添加以下方法:
//用來把每次更新保存到後端的Session中 $scope.saveOrder = function () { var currentProducts = cart.getProducts(); $http.post(tempOrdersUrl, currentProducts) .success(function (data, status, headers, config) { }).error(function (error) { }).finally(function () { }); } //用來每次刷新向後端Session要數據 $scope.checkSessionGadgets = function(){ $http.get(tempOrdersUrl) .success(function(data){ if(data){ for(var i = 0; i < data.length; i++){ var item = data[i]; cart.pushItem(item); } } }) .error(function(error){ console.log('error checking session: ' + error) ; }); }
而後checkSessionGadgets這個方法就要被運用到主視圖上去,當頁面每次加載的時候調用它。
<body ng-controller='gadgetStoreCtrl' class="container" ng-init="checkSessionGadgets()">
每次向購車添加的時候須要從新更新後端的Session狀態。
$scope.addProductToCart = function (product) { cart.addProduct(product.GadgetID, product.Name, product.Price, product.CategoryID); $scope.saveOrder(); }
每次從購物車一處的時候須要從新更新後端的Session狀態。
$scope.remove = function (id) { cart.removeProduct(id); $scope.saveOrder(); }
在用戶提交訂單的時候,須要一出購物車內的產品,再更新後端的Session狀態。
$scope.sendOrder = function (shippingDetails) { var order = angular.copy(shippingDetails); order.gadgets = cart.getProducts(); $http.post(ordersUrl, order) .success(function (data, status, headers, config) { $scope.data.OrderLocation = headers('Location'); $scope.data.OrderID = data.OrderID; cart.getProducts().length = 0; $scope.saveOrder(); }) .error(function (error) { $scope.data.orderError = error; }).finally(function () { $location.path("/complete"); }); }
待續~~