对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)

时间:2021-10-23 12:58:20

chsakell分享了一个前端使用AngularJS,后端使用ASP.NET Web API的项目。

源码: https://github.com/chsakell/spa-webapi-angularjs
文章:http://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/

这里记录下对此项目的理解。分为如下几篇:

● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(1)--领域、Repository、Service

● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(2)--依赖倒置、Bundling、视图模型验证、视图模型和领域模型映射、自定义handler

● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(3)--主页面布局

● 对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)--Movie增改查以及上传图片

显示带分页过滤条件的Movie

对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)

/关于分页,通常情况下传的参数包括:当前页、页容量
//关于过滤:传过滤字符串
[AllowAnonymous]
[Route("{page:int=0}/{pageSize=3}/{filter?}")] //给路由中的变量赋上初值
public HttpResponseMessage Get(HttpRequestMessage request, int? page, int? pageSize, string filter = null)
{
int currentPage = page.Value; // 当前页
int currentPageSize = pageSize.Value;//页容量 return CreateHttpResponse(request, () =>
{
HttpResponseMessage response = null;
List<Movie> movies = null;
int totalMovies = new int();//总数 if (!string.IsNullOrEmpty(filter)) //如果有过滤条件
{
movies = _moviesRepository.GetAll()
.OrderBy(m => m.ID)
.Where(m => m.Title.ToLower()
.Contains(filter.ToLower().Trim()))
.ToList();
}
else
{
movies = _moviesRepository.GetAll().ToList();
} totalMovies = movies.Count(); //总数量
movies = movies.Skip(currentPage * currentPageSize)
.Take(currentPageSize)
.ToList(); //领域模型转换成视图模型
IEnumerable<MovieViewModel> moviesVM = Mapper.Map<IEnumerable<Movie>, IEnumerable<MovieViewModel>>(movies); PaginationSet<MovieViewModel> pagedSet = new PaginationSet<MovieViewModel>()
{
Page = currentPage,
TotalCount = totalMovies,
TotalPages = (int)Math.Ceiling((decimal)totalMovies / currentPageSize),
Items = moviesVM
}; //再把视图模型和分页等数据放在响应中返回
response = request.CreateResponse<PaginationSet<MovieViewModel>>(HttpStatusCode.OK, pagedSet); return response;
});
}

PagenationSet<T>是对分页和领域模型集合的一个封装。

namespace HomeCinema.Web.Infrastructure.Core
{
public class PaginationSet<T>
{
public int Page { get; set; } public int Count
{
get
{
return (null != this.Items) ? this.Items.Count() : ;
}
} public int TotalPages { get; set; }
public int TotalCount { get; set; } public IEnumerable<T> Items { get; set; }
}
}

界面部分,首页中使用了一个自定义的directive。

<side-bar></side-bar>

在side-bar所对应的html部分提供了获取所有Movie的链接。

<a ng-href="#/movies/">Movies<i class="fa fa-film fa-fw pull-right"></i></a>

而在app.js的路由设置中:

.when("/movies", {
templateUrl: "scripts/spa/movies/movies.html",
controller: "moviesCtrl"
})

先来看scripts/spa/movies/movies.html页面摘要:

<!--过滤-->
<input id="inputSearchMovies" type="search" ng-model="filterMovies" placeholder="Filter, search movies..">
<button class="btn btn-primary" ng-click="search();"></button>
<button class="btn btn-primary" ng-click="clearSearch();"></button> <!--列表-->
<a class="pull-left" ng-href="#/movies/{{movie.ID}}" title="View {{movie.Title}} details">
<img class="media-object" height="120" ng-src="../../Content/images/movies/{{movie.Image}}" alt="" />
</a>
<h4 class="media-heading">{{movie.Title}}</h4>
<strong>{{movie.Director}}</strong>
<strong>{{movie.Writer}}</strong>
<strong>{{movie.Producer}}</strong>
<a class="fancybox-media" ng-href="{{movie.TrailerURI}}">Trailer<i class="fa fa-video-camera fa-fw"></i></a>
<span component-rating="{{movie.Rating}}"></span>
<label class="label label-info">{{movie.Genre}}</label>
<available-movie is-available="{{movie.IsAvailable}}"></available-movie> <!--分页-->
<custom-pager page="{{page}}" custom-path="{{customPath}}" pages-count="{{pagesCount}}" total-count="{{totalCount}}" search-func="search(page)"></custom-pager>

再来看对应的moviesCtrl控制器:

(function (app) {
'use strict'; app.controller('moviesCtrl', moviesCtrl); moviesCtrl.$inject = ['$scope', 'apiService','notificationService']; //所有变量都赋初值pageClass, loadingMovies, page, pagesCount, movies, search方法,clearSearch方法
function moviesCtrl($scope, apiService, notificationService) {
$scope.pageClass = 'page-movies';
$scope.loadingMovies = true;
$scope.page = 0;
$scope.pagesCount = 0; $scope.Movies = []; $scope.search = search;
$scope.clearSearch = clearSearch; //当前页索引作为参数传递
function search(page) {
page = page || 0; $scope.loadingMovies = true; //这里的object键值将被放在路由中以便action方法接收
var config = {
params: {
page: page,
pageSize: 6,
filter: $scope.filterMovies
}
}; apiService.get('/api/movies/', config,
moviesLoadCompleted,
moviesLoadFailed);
} function moviesLoadCompleted(result) {
$scope.Movies = result.data.Items;
$scope.page = result.data.Page;
$scope.pagesCount = result.data.TotalPages;
$scope.totalCount = result.data.TotalCount;
$scope.loadingMovies = false; if ($scope.filterMovies && $scope.filterMovies.length)
{
notificationService.displayInfo(result.data.Items.length + ' movies found');
} } function moviesLoadFailed(response) {
notificationService.displayError(response.data);
} function clearSearch() {
$scope.filterMovies = '';
search();
} $scope.search();
} })(angular.module('homeCinema'));

然后对于分页,当然需要自定义directive,如下:

<custom-pager page="{{page}}" custom-path="{{customPath}}" pages-count="{{pagesCount}}" total-count="{{totalCount}}" search-func="search(page)"></custom-pager>

对应的html部分来自spa/layout/pager.html

<div>
<div ng-hide="(!pagesCount || pagesCount < 2)" style="display:inline">
<ul class="pagination pagination-sm">
<li><a ng-hide="page == 0" ng-click="search(0)"><<</a></li>
<li><a ng-hide="page == 0" ng-click="search(page-1)"><</a></li>
<li ng-repeat="n in range()" ng-class="{active: n == page}">
<a ng-click="search(n)" ng-if="n != page">{{n+1}}</a>
<span ng-if="n == page">{{n+1}}</span>
</li>
<li><a ng-hide="page == pagesCount - 1" ng-click="search(pagePlus(1))">></a></li>
<li><a ng-hide="page == pagesCount - 1" ng-click="search(pagesCount - 1)">>></a></li>
</ul>
</div>
</div>

自定义的directive部分写在了spa/layout/customPager.directive.js里:

(function(app) {
'use strict'; app.directive('customPager', customPager); function customPager() {
return {
scope: {
page: '@',
pagesCount: '@',
totalCount: '@',
searchFunc: '&',
customPath: '@'
},
replace: true,
restrict: 'E',
templateUrl: '/scripts/spa/layout/pager.html',
controller: ['$scope', function ($scope) {
$scope.search = function (i) {
if ($scope.searchFunc) {
$scope.searchFunc({ page: i });
}
}; $scope.range = function () {
if (!$scope.pagesCount) { return []; }
var step = 2;
var doubleStep = step * 2;
var start = Math.max(0, $scope.page - step);
var end = start + 1 + doubleStep;
if (end > $scope.pagesCount) { end = $scope.pagesCount; } var ret = [];
for (var i = start; i != end; ++i) {
ret.push(i);
} return ret;
}; $scope.pagePlus = function(count)
{
return +$scope.page + count;
}
}]
}
} })(angular.module('common.ui'));

点击Movie列表中某一项的图片来到明细页

html部分:

<a class="pull-left" ng-href="#/movies/{{movie.ID}}" title="View {{movie.Title}} details">
<img class="media-object" height="120" ng-src="../../Content/images/movies/{{movie.Image}}" alt="" />
</a>

在app.js中已经定义了路由规则:

.when("/movies/:id", {
templateUrl: "scripts/spa/movies/details.html",
controller: "movieDetailsCtrl",
resolve: { isAuthenticated: isAuthenticated }
})

来看API部分:

[Route("details/{id:int}")]
public HttpResponseMessage Get(HttpRequestMessage request, int id)
{
return CreateHttpResponse(request, () =>
{
HttpResponseMessage response = null;
var movie = _moviesRepository.GetSingle(id); MovieViewModel movieVM = Mapper.Map<Movie, MovieViewModel>(movie); response = request.CreateResponse<MovieViewModel>(HttpStatusCode.OK, movieVM); return response;
});
}

再来看明细部分的页面摘要:scripts/spa/movies/details.html

<h5>{{movie.Title}}</h5>
<div class="panel-body" ng-if="!loadingMovie">
<a class="pull-right" ng-href="#/movies/{{movie.ID}}" title="View {{movie.Title}} details">
<img class="media-object" height="120" ng-src="../../Content/images/movies/{{movie.Image}}" alt="" />
</a>
<h4 class="media-heading">{{movie.Title}}</h4>
Directed by: <label>{{movie.Director}}</label><br />
Written by: <label>{{movie.Writer}}</label><br />
Produced by: <label>{{movie.Producer}}</label><br />
Rating: <span component-rating='{{movie.Rating}}'></span>
<br />
<label class="label label-info">{{movie.Genre}}</label>
<available-movie is-available="{{movie.IsAvailable}}"></available-movie> <a ng-href="{{movie.TrailerURI}}" >View Trailer <i class="fa fa-video-camera pull-right"></i></a>
<a ng-href="#/movies/edit/{{movie.ID}}" class="btn btn-default">Edit movie </a>
</div>

控制器部分为:scripts/spa/movies/movieDetailsCtrl.js

(function (app) {
'use strict'; app.controller('movieDetailsCtrl', movieDetailsCtrl); movieDetailsCtrl.$inject = ['$scope', '$location', '$routeParams', '$modal', 'apiService', 'notificationService']; function movieDetailsCtrl($scope, $location, $routeParams, $modal, apiService, notificationService) {
$scope.pageClass = 'page-movies';
$scope.movie = {};
$scope.loadingMovie = true;
$scope.loadingRentals = true;
$scope.isReadOnly = true;
$scope.openRentDialog = openRentDialog;
$scope.returnMovie = returnMovie;
$scope.rentalHistory = [];
$scope.getStatusColor = getStatusColor;
$scope.clearSearch = clearSearch;
$scope.isBorrowed = isBorrowed; function loadMovie() { $scope.loadingMovie = true; apiService.get('/api/movies/details/' + $routeParams.id, null,
movieLoadCompleted,
movieLoadFailed);
} function movieLoadCompleted(result) {
$scope.movie = result.data;
$scope.loadingMovie = false;
} function movieLoadFailed(response) {
notificationService.displayError(response.data);
} loadMovie();
} })(angular.module('homeCinema'));

更新

对一个前端使用AngularJS后端使用ASP.NET Web API项目的理解(4)

在Movie的明细页给出了编辑按钮:

<a ng-href="#/movies/edit/{{movie.ID}}" class="btn btn-default">Edit movie </a>

而在app.js的路由设置中:

.when("/movies/edit/:id", {
templateUrl: "scripts/spa/movies/edit.html",
controller: "movieEditCtrl"
})

来看编辑明细页摘要:scripts/spa/movies/edit.html

<img ng-src="../../Content/images/movies/{{movie.Image}}" class="avatar img-responsive" alt="avatar">
<input type="file" ng-file-select="prepareFiles($files)"> <form class="form-horizontal" role="form" novalidate angular-validator name="editMovieForm" angular-validator-submit="UpdateMovie()"> <input class="form-control" name="title" type="text" ng-model="movie.Title" validate-on="blur" required required-message="'Movie title is required'"> <select ng-model="movie.GenreId" class="form-control black" ng-options="option.ID as option.Name for option in genres" required></select>
<input type="hidden" name="GenreId" ng-value="movie.GenreId" /> <input class="form-control" type="text" ng-model="movie.Director" name="director" validate-on="blur" required required-message="'Movie director is required'"> <input class="form-control" type="text" ng-model="movie.Writer" name="writer" validate-on="blur" required required-message="'Movie writer is required'"> <input class="form-control" type="text" ng-model="movie.Producer" name="producer" validate-on="blur" required required-message="'Movie producer is required'"> <input type="text" class="form-control" name="dateReleased" datepicker-popup="{{format}}" ng-model="movie.ReleaseDate" is-open="datepicker.opened" datepicker-options="dateOptions" ng-required="true" datepicker-append-to-body="true" close-text="Close" validate-on="blur" required required-message="'Date Released is required'" />
<span class="input-group-btn">
<button type="button" class="btn btn-default" ng-click="openDatePicker($event)"></button>
</span> <input class="form-control" type="text" ng-model="movie.TrailerURI" name="trailerURI" validate-on="blur" required required-message="'Movie trailer is required'" ng-pattern="/^(https?\:\/\/)?(www\.youtube\.com|youtu\.?be)\/.+$/" invalid-message="'You must enter a valid YouTube URL'"> <textarea class="form-control" ng-model="movie.Description" name="description" rows="3" validate-on="blur" required required-message="'Movie description is required'" /> <span component-rating="{{movie.Rating}}" ng-model="movie.Rating" class="form-control"></span> <input type="submit" class="btn btn-primary" value="Update" />
<a class="btn btn-default" ng-href="#/movies/{{movie.ID}}">Cancel</a>
</form>

再来看编辑明细页控制器:scripts/spa/movies/movieEditCtrl.js

(function (app) {
'use strict'; app.controller('movieEditCtrl', movieEditCtrl); movieEditCtrl.$inject = ['$scope', '$location', '$routeParams', 'apiService', 'notificationService', 'fileUploadService']; function movieEditCtrl($scope, $location, $routeParams, apiService, notificationService, fileUploadService) {
$scope.pageClass = 'page-movies';
$scope.movie = {};
$scope.genres = [];
$scope.loadingMovie = true;
$scope.isReadOnly = false;
$scope.UpdateMovie = UpdateMovie;
$scope.prepareFiles = prepareFiles;
$scope.openDatePicker = openDatePicker; $scope.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
$scope.datepicker = {}; var movieImage = null; //加载movie
function loadMovie() { $scope.loadingMovie = true; apiService.get('/api/movies/details/' + $routeParams.id, null,
movieLoadCompleted,
movieLoadFailed);
} //加载movie完成后加载genre
function movieLoadCompleted(result) {
$scope.movie = result.data;
$scope.loadingMovie = false; //再加载genre
loadGenres();
} function movieLoadFailed(response) {
notificationService.displayError(response.data);
} function genresLoadCompleted(response) {
$scope.genres = response.data;
} function genresLoadFailed(response) {
notificationService.displayError(response.data);
} function loadGenres() {
apiService.get('/api/genres/', null,
genresLoadCompleted,
genresLoadFailed);
} function UpdateMovie() {
//上传图片
if (movieImage) {
fileUploadService.uploadImage(movieImage, $scope.movie.ID, UpdateMovieModel);
}
else
UpdateMovieModel();
} //实施更新
function UpdateMovieModel() {
apiService.post('/api/movies/update', $scope.movie,
updateMovieSucceded,
updateMovieFailed);
} function prepareFiles($files) {
movieImage = $files;
} function updateMovieSucceded(response) {
console.log(response);
notificationService.displaySuccess($scope.movie.Title + ' has been updated');
$scope.movie = response.data;
movieImage = null;
} function updateMovieFailed(response) {
notificationService.displayError(response);
} function openDatePicker($event) {
$event.preventDefault();
$event.stopPropagation(); $scope.datepicker.opened = true;
}; loadMovie();
} })(angular.module('homeCinema'));

对于上传图片,放在了一个自定义的服务中,在spa/services/fileUploadService.js中:

(function (app) {
'use strict'; app.factory('fileUploadService', fileUploadService); fileUploadService.$inject = ['$rootScope', '$http', '$timeout', '$upload', 'notificationService']; function fileUploadService($rootScope, $http, $timeout, $upload, notificationService) { $rootScope.upload = []; var service = {
uploadImage: uploadImage
} function uploadImage($files, movieId, callback) {
//$files: an array of files selected
for (var i = 0; i < $files.length; i++) {
var $file = $files[i];
(function (index) {
$rootScope.upload[index] = $upload.upload({
url: "api/movies/images/upload?movieId=" + movieId, // webapi url
method: "POST",
file: $file
}).progress(function (evt) {
}).success(function (data, status, headers, config) {
// file is uploaded successfully
notificationService.displaySuccess(data.FileName + ' uploaded successfully');
callback();
}).error(function (data, status, headers, config) {
notificationService.displayError(data.Message);
});
})(i);
}
} return service;
} })(angular.module('common.core'));

在后端API中,对应的更新和上传图片action如下:

[HttpPost]
[Route("update")]
public HttpResponseMessage Update(HttpRequestMessage request, MovieViewModel movie)
{
return CreateHttpResponse(request, () =>
{
HttpResponseMessage response = null; if (!ModelState.IsValid)
{
response = request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
else
{
var movieDb = _moviesRepository.GetSingle(movie.ID);
if (movieDb == null)
response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid movie.");
else
{
movieDb.UpdateMovie(movie);
movie.Image = movieDb.Image;
_moviesRepository.Edit(movieDb); _unitOfWork.Commit();
response = request.CreateResponse<MovieViewModel>(HttpStatusCode.OK, movie);
}
} return response;
});
} [MimeMultipart]
[Route("images/upload")]
public HttpResponseMessage Post(HttpRequestMessage request, int movieId)
{
return CreateHttpResponse(request, () =>
{
HttpResponseMessage response = null; var movieOld = _moviesRepository.GetSingle(movieId);
if (movieOld == null)
response = request.CreateErrorResponse(HttpStatusCode.NotFound, "Invalid movie.");
else
{
var uploadPath = HttpContext.Current.Server.MapPath("~/Content/images/movies"); var multipartFormDataStreamProvider = new UploadMultipartFormProvider(uploadPath); // Read the MIME multipart asynchronously
Request.Content.ReadAsMultipartAsync(multipartFormDataStreamProvider); string _localFileName = multipartFormDataStreamProvider
.FileData.Select(multiPartData => multiPartData.LocalFileName).FirstOrDefault(); // Create response
FileUploadResult fileUploadResult = new FileUploadResult
{
LocalFilePath = _localFileName, FileName = Path.GetFileName(_localFileName), FileLength = new FileInfo(_localFileName).Length
}; // update database
movieOld.Image = fileUploadResult.FileName;
_moviesRepository.Edit(movieOld);
_unitOfWork.Commit(); response = request.CreateResponse(HttpStatusCode.OK, fileUploadResult);
} return response;
});
}

添加

在sidebar的html部分为:

<li><a ng-href="#/movies/add">Add movie</a></li>

在app.js的路由设置中:

.when("/movies/add", {
templateUrl: "scripts/spa/movies/add.html",
controller: "movieAddCtrl",
resolve: { isAuthenticated: isAuthenticated }
})

scripts/spa/movies/add.html部分与edit.html部分相似。

scripts/spa/movies/movieAddCtrl.js中:

(function (app) {
'use strict'; app.controller('movieAddCtrl', movieAddCtrl); movieAddCtrl.$inject = ['$scope', '$location', '$routeParams', 'apiService', 'notificationService', 'fileUploadService']; function movieAddCtrl($scope, $location, $routeParams, apiService, notificationService, fileUploadService) { $scope.pageClass = 'page-movies';
$scope.movie = { GenreId: 1, Rating: 1, NumberOfStocks: 1 }; $scope.genres = [];
$scope.isReadOnly = false;
$scope.AddMovie = AddMovie;
$scope.prepareFiles = prepareFiles;
$scope.openDatePicker = openDatePicker;
$scope.changeNumberOfStocks = changeNumberOfStocks; $scope.dateOptions = {
formatYear: 'yy',
startingDay: 1
};
$scope.datepicker = {}; var movieImage = null; function loadGenres() {
apiService.get('/api/genres/', null,
genresLoadCompleted,
genresLoadFailed);
} function genresLoadCompleted(response) {
$scope.genres = response.data;
} function genresLoadFailed(response) {
notificationService.displayError(response.data);
} function AddMovie() {
AddMovieModel();
} function AddMovieModel() {
apiService.post('/api/movies/add', $scope.movie,
addMovieSucceded,
addMovieFailed);
} function prepareFiles($files) {
movieImage = $files;
} function addMovieSucceded(response) {
notificationService.displaySuccess($scope.movie.Title + ' has been submitted to Home Cinema');
$scope.movie = response.data; //添加movie成功后再上传图片
if (movieImage) {
fileUploadService.uploadImage(movieImage, $scope.movie.ID, redirectToEdit);
}
else
redirectToEdit();
} function addMovieFailed(response) {
console.log(response);
notificationService.displayError(response.statusText);
} function openDatePicker($event) {
$event.preventDefault();
$event.stopPropagation(); $scope.datepicker.opened = true;
}; function redirectToEdit() {
$location.url('movies/edit/' + $scope.movie.ID);
} function changeNumberOfStocks($vent)
{
var btn = $('#btnSetStocks'),
oldValue = $('#inputStocks').val().trim(),
newVal = 0; if (btn.attr('data-dir') == 'up') {
newVal = parseInt(oldValue) + 1;
} else {
if (oldValue > 1) {
newVal = parseInt(oldValue) - 1;
} else {
newVal = 1;
}
}
$('#inputStocks').val(newVal);
$scope.movie.NumberOfStocks = newVal;
console.log($scope.movie);
} loadGenres();
} })(angular.module('homeCinema'));

后端对应的添加API部分:

[HttpPost]
[Route("add")]
public HttpResponseMessage Add(HttpRequestMessage request, MovieViewModel movie)
{
return CreateHttpResponse(request, () =>
{
HttpResponseMessage response = null; if (!ModelState.IsValid)
{
response = request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
else
{
Movie newMovie = new Movie();
newMovie.UpdateMovie(movie); for (int i = ; i < movie.NumberOfStocks; i++)
{
Stock stock = new Stock()
{
IsAvailable = true,
Movie = newMovie,
UniqueKey = Guid.NewGuid()
};
newMovie.Stocks.Add(stock);
} _moviesRepository.Add(newMovie); _unitOfWork.Commit(); // Update view model
movie = Mapper.Map<Movie, MovieViewModel>(newMovie);
response = request.CreateResponse<MovieViewModel>(HttpStatusCode.Created, movie);
} return response;
});
}

本系列结束~