原文:购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
chsakell分享了前端使用AngularJS,后端使用ASP.NET Web API的购物车案例,非常精彩,这里这里记录下对此项目的理解。
文章:
http://chsakell.com/2015/01/31/angularjs-feat-web-api/
http://chsakell.com/2015/03/07/angularjs-feat-web-api-enable-session-state/
源码:
https://github.com/chsakell/webapiangularjssecurity
本系列共三篇,本篇是第二篇。
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证
HomeController用来展示主页面,并接受前端传来的Order的编号。
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视图。
<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文件中了。
最终的界面如下:
main.js 声明*module,以及配置路由。
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");
});
}
待续~~