目录
在Maser Chef第1部分中,我介绍了如何将ASP.NET Core MVC与Fluent NHibernate和Angular JS集成。在本文中,我将讨论如何使用ASP.NET Core MVC,Fluent NHibernate和Angular JS来实现CRUD SPA(单页应用程序)。
在存储库中使用泛型
创建、读取、更新和删除(缩写为CRUD)是持久性存储的四个基本功能。
我们首先需要在我们的存储库类中的数据库级别实现CRUD。我想将泛型用于查询,添加、更新、删除方法,以避免冗余编码。为什么要使用泛型?简短的答案是类型安全的编译时检查,速度更快,适用于具有相同基础行为的许多类型。
在以前的数据模型类中,所有成员的名称与数据库字段的名称相同。实际上,数据模型类成员不必与数据库字段相同。例如,Recipe类的ID不必是RecipeId,它可以是任何名称,例如Id。我们需要做的是在映射过程中告诉Fluent NHibernate,如下所示。
Id(x => x.Id, "RecipeId");
这样Fluent NHibernate知道它正在将“Id
” 映射到“RecipeId
”。
因为我们不必使用与数据库字段相同的名称,所以现在我们有机会更改不同的数据模型类以具有一些公共成员。
因此,我们创建了一个Entity
基类。
public class Entity
{
public virtual Guid Id { get; set; }
public virtual Guid? ParentId { get; set; }
public virtual Type ParentType => null;
}
然后我们使Recipe
,RecipeStep
和RecipeItem
继承Entity
和使用Id
更换Recipe
的RecipeId
,使用Id
更换RecipeStep
的RecipeStepId
和使用Id
更换RecipeItem
的ItemId
。同时使用ParentId
更换RecipeStep
的RecipeId
和使用ParentId
更换RecipeItem
的RecipeStepId
。
public class Recipe : Entity
{
public virtual string Name { get; set; }
public virtual string Comments { get; set; }
public virtual DateTime ModifyDate { get; set; }
public virtual IList<RecipeStep> Steps { get; set; }
}
public class RecipeStep : Entity
{
public virtual int StepNo { get; set; }
public virtual string Instructions { get; set; }
public virtual IList<RecipeItem> RecipeItems { get; set; }
public override Type ParentType => typeof(Recipe);
}
public class RecipeItem : Entity
{
public virtual string Name { get; set; }
public virtual decimal Quantity { get; set; }
public virtual string MeasurementUnit { get; set; }
public override Type ParentType => typeof(RecipeStep);
}
现在,我们还需要更改映射类。请注意不同名称的映射。
public class RecipeMap : ClassMap<Recipe>
{
public RecipeMap()
{
Id(x => x.Id, "RecipeId");
Map(x => x.Name);
Map(x => x.Comments);
Map(x => x.ModifyDate);
HasMany(x => x.Steps).KeyColumn("RecipeId").Inverse().Cascade.DeleteOrphan().OrderBy("StepNo Asc");
Table("Recipes");
}
}
public class RecipeStepMap : ClassMap<RecipeStep>
{
public RecipeStepMap()
{
Id(x => x.Id, "RecipeStepId");
Map(x => x.ParentId, "RecipeId");
Map(x => x.StepNo);
Map(x => x.Instructions);
HasMany(x => x.RecipeItems).KeyColumn("RecipeStepId").Inverse().Cascade.DeleteOrphan();
Table("RecipeSteps");
}
}
public class RecipeItemMap : ClassMap<RecipeItem>
{
public RecipeItemMap()
{
Id(x => x.Id, "ItemId");
Map(x => x.Name);
Map(x => x.Quantity);
Map(x => x.MeasurementUnit);
Map(x => x.ParentId, "RecipeStepId");
Table("RecipeItems");
}
}
什么是“Cascade.DeleteOrphan
”?当您删除父对象时,此选项将删除子对象。对于我们的情况,删除配方将删除该配方的所有配方步骤和配方项,而删除一个步骤将删除此步骤的所有项。
然后,将Repository
的方法更改为泛型方法,并放置通用约束,该约束T
必须是Entity的子类。
public T GetEntity<T>(Guid id) where T : Entity
{
try
{
return _session.Get<T>(id);
}
catch (Exception ex)
{
throw ex;
}
}
public T AddEntity<T>(T entity) where T : Entity
{
T newOne = null;
using (var transaction = _session.BeginTransaction())
{
try
{
_session.SaveOrUpdate(entity);
Commit(transaction, entity);
RefreshParentObject(entity);
newOne = _session.Get<T>(entity.Id) as T;
}
catch (Exception ex)
{
throw ex;
}
return newOne;
}
}
public void UpdateEntity<T>(T entity) where T : Entity
{
using (var transaction = _session.BeginTransaction())
{
try
{
_session.Update(entity);
Commit(transaction, entity);
RefreshParentObject(entity);
}
catch (Exception ex)
{
throw ex;
}
}
}
public void DeleteEntity<T>(Guid id) where T : Entity
{
using (var transaction = _session.BeginTransaction())
{
var entity = _session.Get<T>(id);
if (entity != null)
{
try
{
_session.Delete(entity);
Commit(transaction, entity);
RefreshParentObject(entity);
}
catch (Exception ex)
{
throw ex;
}
}
}
}
对于添加、更新和删除方法,所有这些都调用RefreshParentObject()
。这意味着什么?当我们更改RecipeStep
或RecipeItem
时,其父对象缓存不知道此更改。我们需要刷新父对象缓存。
void RefreshParentObject(Entity entity)
{
if (!entity.ParentId.HasValue)
return;
var parentObj = _session.Get(entity.ParentType, entity.ParentId.Value);
if (parentObj != null)
_session.Refresh(parentObj);
}
现在,我们更新Web API控制器。
[HttpGet("{id}")]
public IActionResult Get(Guid id)
{
var recipe = _repository.GetEntity<Recipe>(id);
if (recipe != null)
return new ObjectResult(recipe);
else
return new NotFoundResult();
}
[HttpPost]
public IActionResult Post([FromBody]Recipe recipe)
{
if (recipe.Id == Guid.Empty)
{
recipe.ModifyDate = DateTime.Now;
return new ObjectResult(_repository.AddEntity<Recipe>(recipe));
}
else
{
var existingOne = _repository.GetEntity<Recipe>(recipe.Id);
existingOne.Name = recipe.Name;
existingOne.Comments = recipe.Comments;
existingOne.ModifyDate = DateTime.Now;
_repository.UpdateEntity<Recipe>(existingOne);
return new ObjectResult(existingOne);
}
}
[HttpPut("{id}")]
public IActionResult Put(Guid id, [FromBody]Recipe recipe)
{
var existingOne = _repository.GetEntity<Recipe>(recipe.Id);
existingOne.Name = recipe.Name;
existingOne.Comments = recipe.Comments;
_repository.UpdateEntity<Recipe>(recipe);
return new ObjectResult(existingOne);
}
[HttpDelete("{id}")]
public IActionResult Delete(Guid id)
{
_repository.DeleteEntity<Recipe>(id);
return new StatusCodeResult(200);
}
Angular客户端路由
现在,我们需要在Master Chef应用程序中设置客户端路由,以便可以根据客户端提供的URL替换动态视图。我们可以从angular-route模块中获取Angular路由功能。
使用ngRoute
模块,您可以在单个页面应用程序中导航到不同的页面,而无需使用页面reloading.$route
来深度链接url到控制器和视图(HTML部分)。它监视$location.url()
并尝试将路径映射到现有路由定义。
有两个依赖$route
,$location
和$routeParams
。
1)注入ngRoute
打开app.js,在我们的masterChefApp
模块中注入ngroute
。
(function () {
'use strict';
angular.module('masterChefApp', [
// Angular modules
'ngRoute',
// Custom modules
'recipesService'
// 3rd Party Modules
]);
})();
2)配置Angular路由
为我们的Angular应用程序模块定义配置函数——masterChefApp
。并且,在该配置函数中,使用来自ngRoute
模块的路由提供程序服务来定义客户端路由
angular.module('masterChefApp').config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) {
$routeProvider
.when('/', {
templateUrl: 'partials/recipes.html',
controller: 'recipesController'
})
.when('/recipes/add', {
templateUrl: 'partials/add.html',
controller: 'recipesAddController'
})
.when('/recipes/edit/:id', {
templateUrl: 'partials/edit.html',
controller: 'recipesEditController'
})
.when('/recipes/delete/:id', {
templateUrl: 'partials/delete.html',
controller: 'recipesDeleteController'
});
$locationProvider.html5Mode(true);
}]);
第一个只是默认路由——斜杠。第二个是/recipes/add。第三个是/recipes/edit /并传递:id
作为路由参数,这使我们可以获取一个动态ID,该ID可与一种配方匹配。最后一条路由/recipes/delete/:id也需要采用动态ID参数。该默认途径只是列出所有食谱。“Add
”路由将用于添加,“Edit
”路由将用于编辑或更新,“Delete
“路由将要删除或移除。CRUD函数由这四个客户端路由表示。对于每个路由,我们都需要定义一个模板URL(表示应为此路由呈现的一些HTML)以及还有一个单独的控制器来处理这条路由。
在最底端,使用$locationProvider
,它的html5Mode
函数,
设置为true以确保我可以使用友好自然的URL并避免将哈希爆炸用于客户端路由。
Angular JS客户端控制器
我们已经配置了默认路由,添加路由,编辑路由和删除路由。然后,我们需要相应的控制器recipesController
,recipesAddController
,recipesEditController
和recipesDeleteController
。我们在recipesController.js中定义所有这些控制器。
1)注入“Add”,“Edit”和“Delete”控制器
angular
.module('masterChefApp')
.controller('recipesController', recipesController)
.controller('recipesAddController', recipesAddController)
.controller('recipesEditController', recipesEditController)
.controller('recipesDeleteController', recipesDeleteController);
2)实现食谱Add控制器
recipesAddController.$inject = ['$scope', 'Recipe', '$location'];
function recipesAddController($scope, Recipe, $location) {
$scope.recipe = new Recipe();
$scope.addRecipe = function () {
$scope.recipe.$save(function () {
$location.path('/');
});
}
}
因此recipesAddController
需要$scope
和配方服务,它也需要$location
服务。recipesAddController
创建或提供允许某人向应用程序添加配方的功能。为此,请使用“配方”服务创建一个新的$scope
变量配方。它还在此处创建一个$scope
函数——addRecipe
,它将通过使用配方服务save方法将配方提交给服务器。在提交配方后的回调中,我们仅要将应用程序重定向到其主页。
3)实现食谱Edit控制器
recipesEditController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesEditController($scope, Recipe, $location, $routeParams) {
$scope.recipe = Recipe.get({ id: $routeParams.id });
$scope.editRecipe = function () {
$scope.recipe.$save(function () {
$location.path('/');
});
}
}
recipesEditController
需要$scope
和配方服务,$location
服务。它也需要$routeParameter
来传递id
。recipesEditController
创建或提供允许某人将配方更新到应用程序的功能。我们将使用&routeParams
服务来更新配方。从route参数获取配方ID。然后,我们将通过调用配方服务get函数进入服务器并获取适当的配方——这次是提供ID的get方法。这将提供给前端。用户将能够制造任何东西。
最后,我们将更新的配方记录提交到服务器。
4)实现食谱Delete控制器
recipesDeleteController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesDeleteController($scope, Recipe, $location, $routeParams) {
$scope.recipe = Recipe.get({ id: $routeParams.id });
$scope.deleteRecipe = function () {
$scope.recipe.$remove({ id: $scope.recipe.id }, function () {
$location.path('/');
});
};
}
recipesDeleteController
使用$routeParams
获取ID并检索特定配方。然后提供此函数deleteRecipe
,在这里我们可以使用Recipe服务的$remove
方法告诉服务器我们要摆脱特定的食谱。
部分视图模板
1)修改Index.html以使用ng-view
修改index.html以使用部分视图。首先向/添加一个“基本”标签及其href属性。这是必需的,这样$locationProvider
才能正常工作,因为它需要一个基础才能工作。现在转到正文内容。摆脱所有这些,只需使用ng-view指令。
<!DOCTYPE html>
<html ng-app="masterChefApp">
<head>
<base href="/">
<meta charset="utf-8" />
<title>Master Chef Recipes</title>
<script src="lib/angular/angular.min.js"></script>
<script src="lib/angular-resource/angular-resource.min.js"></script>
<script src="lib/angular-route/angular-route.min.js"></script>
<script src="app.js"></script>
</head>
<body ng-cloak>
<div>
<ng-view></ng-view>
</div>
</body>
</html>
基于此ng-view指令的使用以及我们已经设置的路由,ng-view将能够提供正确的局部视图以及正确的控制器,以便在任何客户端路由上使用$routeProvider
为视图提供动力。
我们在app.js文件中指定了四个控制器。这些控制器为我们提供了CRUD操作。路径URL /将要从服务器检索所有配方。 /recipes/add将创建一个新配方。具有可变ID的recipes/edit将更新现有配方,并且具有可变ID的/recipes/delete 也将从服务器中删除或移除特定配方。
现在,我们在wwwroot文件夹下创建“partials”文件夹。然后可以一个一个地添加模板。
2)检索模板——Recipes.html
右键单击wwwroot下的“partials”文件夹。添加一个新项目。在“客户端模板”部分中,选择“HTML页面”。我们将其命名为“recipes.html ”。
recipes.html,它将检索并显示食谱列表。
<div>
<h2>Master Chief Recipes</h2>
<ul>
<li ng-repeat="recipe in recipes">
<div>
<h5>{{recipe.name}} - {{recipe.comments}}</h5>
</div>
<div>
<a href="recipes/edit/{{recipe.id}}">edit</a>
</div>
<div>
<a href="recipes/delete/{{recipe.id}}">delete</a>
</div>
<ul>
<li ng-repeat="step in recipe.steps">
<p> step {{step.stepNo}} : {{step.instructions}}</p>
<ul>
<li ng-repeat="item in step.recipeItems">
<p> {{item.name}} {{item.quantity}} {{item.measurementUnit}}</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><a href="recipes/add"> Add a new recipe </a></p>
</div>
请注意,这不是完整的html。我们只是在定义一个局部视图,该视图将在AngularJS应用程序中替换。
现在,如果我们运行它,我们应该看到所有配方。
3)Bootstrap 样式
尽管它可以正常工作,但是它是完全纯HTML。因此,我们需要应用一些CSS样式。
Bootstrap是一个非常流行的前端框架,它包括用于排版、表单、按钮、表格、导航、模式、图像轮播等的基于HTML和CSS的设计模板,以及可选的JavaScript插件。应用bootstrap样式可以使我们的Master Web应用程序更漂亮。
我们已经在Bower配置中添加了bootstrap软件包。
{
"name": "asp.net",
"private": true,
"dependencies": {
"jquery": "3.1.0",
"bootstrap": "3.1.0",
"angular": "1.5.8",
"angular-route": "1.5.8",
"angular-resource": "1.5.8"
}
}
因此,引导程序已经安装在wwwroot\lib文件夹中。现在我们将其包含在index.html中。
<link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" media="screen">
我们将应用以下bootstrap样式。
我们将index.html中的主div与.container-fluid(full-width)一起使用,以实现正确的对齐和填充。
我们在cipes.html中将全部.list-group
用于ui .list-group-item
用于li 。我们还对添加链接应用“btn-primary
”,对编辑链接应用“btn-default
”,对删除链接应用“btn-delete
”。我也想将食谱显示为徽章,所以也要应用.badge
样式。
再次运行Master Chef,看看现在是什么样。
Bootstrap包括功能强大的移动优先网格系统,用于构建各种形状和尺寸的布局。它基于12列布局,并具有多层,每个层对应一个媒体查询范围。有三个主要组件——容器、行和列。容器——固定宽度的.container
或全宽度的.container-fluid
——将站点内容居中,并帮助对齐网格内容。行是水平的列组,可确保您的列正确排列。Column类表示每行可能使用的12列中的列数。因此,如果要三个相等宽度的列,则可以使用.col-xs-4。
我们在Master Chef模板中使用bootstrap网格系统。
4)使用Angular JS进行扩展/折叠
我知道有很多方法可以与jQuery进行扩展/折叠以更改DOM。但是请记住,我们使用的是AngularJS的MVVM模式。因此,我很想通过更改控制器(视图模型)中的模型来实现扩展/折叠。
在recipesController
中添加expand()
函数。在expand()
函数中,设置配方对象的show属性。
recipesController.$inject = ['$scope', 'Recipe'];
function recipesController($scope, Recipe) {
$scope.recipes = Recipe.query();
$scope.expand = function (recipe) {
recipe.show = !recipe.show;
}
}
我们在recipesController
中添加了ng-click调用expand()
函数。
<div class="btn-group">
<button class="btn badge pull-left" ng-click="expand(recipe)"><h5>{{recipe.name}} - {{recipe.comments}}</h5></button>
</div>
然后,我们使用ng-show来控制是否显示食谱的详细信息。
<ul class="list-group" ng-show="recipe.show">
<li ng-repeat="step in recipe.steps" class="list-group-item">
只需单击配方标记即可展开您想要的外观。
5)创建模板——add.html
右键单击wwwroot下的“partials”文件夹。添加一个新项目。在“客户端模板”部分中,选择“HTML页面”。我们将其命名为“add.html”。
在add.html中,使用ng-submit将数据发布到服务器。我们将通过ng-model指令将用户在输入字段中输入的信息绑定到范围变量配方。当用户通过单击“保存”按钮使用我们的表单提交时,我们将调用一个作用域函数addRecipe
,该函数在我们控制器的后台会将该配方对象提交给服务器。
<h1>Add a new recipe</h1>
<div class="container-fluid">
<form ng-submit="addRecipe()">
<div class="row">
<div class="form-group col-xs-4">
<label for="name">Name</label>
<input ng-model="recipe.name" name="name" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-8">
<label for="comments">Comments</label>
<input ng-model="recipe.comments" name="comments" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
6)编辑模板——edit.html
右键单击wwwroot下的“partials”文件夹。添加一个新项目。在“客户端模板”部分中,选择“HTML页面”。我们命名为“edit.html ”。
现在我们要更新配方。我们将在edit.html部分模板中进行处理。edit.html看起来像add.html,因为我们需要为最终用户提供所有必要的字段以实际更新现有配方。我们有配方.name和配方.comments的输入。它们通过ng-model指令绑定到范围变量(对象配方)。另外,在编辑控制器上,还有一个作用域函数——editRecipe
。因此,当用户按下edit.html中的Save按钮时,该函数将被调用,而该函数的工作就是将更新的配方信息提交给服务器以进行持久存储。
<h1>Edit recipe</h1>
<div class="container-fluid">
<form ng-submit="editRecipe()">
<div class="row">
<div class="form-group col-xs-4">
<label for="name">Name</label>
<input ng-model="recipe.name" name="name" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-8">
<label for="comments">Comments</label>
<input ng-model="recipe.comments" name="comments" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
7)删除模板
右键单击wwwroot下的“partials”文件夹。添加一个新项目。在“客户端模板”部分中,选择“HTML页面”。我们给它起一个名字“delete.html ”。
在delete.html中,我们将提供一个段落进行确认。那么,您真的要删除此食谱吗?我们将绑定到要删除的配方相关的配方信息。我们将提供一个按钮,它调用范围函数——deleteRecipe
。它将向服务器提交删除特定配方的请求。
<div class="alert alert-warning">
<p>Do you really want to delete this recipe?</p>
<p> {{recipe.name}} - {{recipe.comments}}</p>
</div>
<button ng-click="deleteRecipe()" class="btn btn-danger">Yes</button>
<a href="/" class="btn btn-default">No</a>
多个URL映射到同一Web API控制器
配方步骤和配方项目如何?一般而言,我们可以制作单独的API控制器来处理配方步骤和配方项目。但这太重了。我想在RecipesController中包装所有与食谱相关的Restful服务。但是对于配方步骤操作和配方项目操作,肯定需要不同的URL。幸运的是,ASP.NET Core Web API支持不同的路由。路由是Web API将URI与操作匹配的方式。Web API支持一种新型的路由,称为属性路由。顾名思义,属性路由使用属性来定义路由。通过属性路由,您可以更好地控制Web API中的URI。例如,您可以轻松创建描述资源层次结构的URI。
Web控制器类的route属性是基本URI。
[Route("api/[controller]")]
public class RecipesController : Controller
{
….
}
对于RecipesController
,基本URL是/api/recipes。
[HttpGet("{id}")]
public IActionResult Get(Guid id)
{
var recipe = _repository.GetEntity<Recipe>(id);
if (recipe != null)
return new ObjectResult(recipe);
else
return new NotFoundResult();
}
上面的方法没有route属性,这意味着该方法已映射到/api/recipes/:id
但是看来我们需要不同的URL来get step方法和get item方法。我想获取步骤URL为/api/recipes/step/:id,获取项目URL为/api/recipes/item/:id。因此,我们为get step方法添加[Route("step/{id}")]
和为get item方法添加[Route("item/{id}")]
。
[HttpGet]
[Route("step/{id}")]
public IActionResult GetStep(Guid id)
{
var recipeStep = _repository.GetEntity<RecipeStep>(id);
if (recipeStep != null)
return new ObjectResult(recipeStep);
else
return new NotFoundResult();
}
[HttpGet]
[Route("item/{id}")]
public IActionResult GetItem(Guid id)
{
var recipeItem = _repository.GetEntity<RecipeItem>(id);
if (recipeItem != null)
return new ObjectResult(recipeItem);
else
return new NotFoundResult();
}
让我们看一下API路由是否有效。单击IIS Express以启动我们的Web应用程序。首先,我们检查URL,api/recipes/step/AEE9602B-03EF-4A5F-A380-2962134ADB7E。
它按预期工作。
然后我们检查api/recipes/item/862B91D5-FB60-4004-8179-0415AB900795
它也在工作。
我们还需要为发布和删除添加Route
属性。
//GET api/recipes/step/:id
[HttpGet]
[Route("step/{id}")]
public IActionResult GetStep(Guid id)
{
var recipeStep = _repository.GetEntity<RecipeStep>(id);
if (recipeStep != null)
return new ObjectResult(recipeStep);
else
return new NotFoundResult();
}
//POST api/recipes/step
[HttpPost]
[Route("step")]
public IActionResult UpdateStep([FromBody]RecipeStep recipeStep)
{
if (recipeStep.Id == Guid.Empty)
{
return new ObjectResult(_repository.AddEntity<RecipeStep>(recipeStep));
}
else
{
var existingOne = _repository.GetEntity<RecipeStep>(recipeStep.Id);
existingOne.StepNo = recipeStep.StepNo;
existingOne.Instructions = recipeStep.Instructions;
_repository.UpdateEntity<RecipeStep>(existingOne);
return new ObjectResult(existingOne);
}
}
//DELETE api/recipes/step/:id
[HttpDelete]
[Route("step/{id}")]
public IActionResult DeleteStep(Guid id)
{
_repository.DeleteEntity<RecipeStep>(id);
return new StatusCodeResult(200);
}
// GET api/recipes/item/:id
[HttpGet]
[Route("item/{id}")]
public IActionResult GetItem(Guid id)
{
var recipeItem = _repository.GetEntity<RecipeItem>(id);
if (recipeItem != null)
return new ObjectResult(recipeItem);
else
return new NotFoundResult();
}
//POST api/recipes/item
[HttpPost]
[Route("item")]
public IActionResult UpdateItem([FromBody]RecipeItem recipeItem)
{
if (recipeItem.Id == Guid.Empty)
{
if (recipeItem.MeasurementUnit == null)
recipeItem.MeasurementUnit = "";
return new ObjectResult(_repository.AddEntity<RecipeItem>(recipeItem));
}
else
{
var existingOne = _repository.GetEntity<RecipeItem>(recipeItem.Id);
existingOne.Name = recipeItem.Name;
existingOne.Quantity = recipeItem.Quantity;
existingOne.MeasurementUnit = recipeItem.MeasurementUnit;
_repository.UpdateEntity<RecipeItem>(existingOne);
return new ObjectResult(existingOne);
}
}
//DELETE api/recipes/item/:id
[HttpDelete]
[Route("item/{id}")]
public IActionResult DeleteItem(Guid id)
{
_repository.DeleteEntity<RecipeItem>(id);
return new StatusCodeResult(200);
}
单个Angular资源服务的多个路由URL
Angular资源服务也支持多个URL。到目前为止,我们仅使用默认操作。
{
get: {method: 'GET'},
save: {method: 'POST'},
query: {method: 'GET', isArray: true},
remove: {method: 'DELETE'},
delete: {method: 'DELETE'}
}
上面的动作是内置在ng资源中的,因此我们可以直接使用它。
recipesService.factory('Recipe', ['$resource', function ($resource) {
return $resource('/api/recipes/:id');
}]);
但是,我们现在需要定义自己的自定义动作,并为该动作指定默认URL的不同URL。
recipesService.factory('Recipe', ['$resource', function ($resource) {
return $resource('/api/recipes/:id', {}, {
getRecipeStep: { method: 'GET', url: '/api/recipes/step/:id' },
saveRecipeStep: { method: 'POST', url: '/api/recipes/step' },
removeRecipeStep: { method: 'DELETE', url: '/api/recipes/step/:id' },
getRecipeItem: { method: 'GET', url: '/api/recipes/item/:id' },
saveRecipeItem: { method: 'POST', url: '/api/recipes/item' },
removeRecipeItem: { method: 'DELETE', url: '/api/recipes/item/:id' }
});
}]);
我们仍然使用默认操作的配方,并添加新的自定义操作getRecipeStep
,saveRecipeStep
,removeRecipeStep
,getRecipeItem
,saveRecipeItem
和removeRecipeItem
。
所有URL均与配方步骤和配方项目的Web API URL匹配。
为配方步骤和配方项目添加新的Angular路由
现在我们需要添加新的客户端路径,用于在app.js中创建、更新、删除配方步骤和配方项创建、更新、删除模板和控制器。
$routeProvider
.when('/', {
templateUrl: 'partials/recipes.html',
controller: 'recipesController'
})
.when('/recipes/add', {
templateUrl: 'partials/add.html',
controller: 'recipesAddController'
})
.when('/recipes/edit/:id', {
templateUrl: 'partials/edit.html',
controller: 'recipesEditController'
})
.when('/recipes/delete/:id', {
templateUrl: 'partials/delete.html',
controller: 'recipesDeleteController'
})
.when('/recipes/addStep/:id', {
templateUrl: 'partials/addStep.html',
controller: 'recipesAddStepController'
})
.when('/recipes/editStep/:id', {
templateUrl: 'partials/editStep.html',
controller: 'recipesEditStepController'
})
.when('/recipes/deleteStep/:id', {
templateUrl: 'partials/deleteStep.html',
controller: 'recipesDeleteStepController'
})
.when('/recipes/addItem/:id', {
templateUrl: 'partials/addItem.html',
controller: 'recipesAddItemController'
})
.when('/recipes/editItem/:id', {
templateUrl: 'partials/editItem.html',
controller: 'recipesEditItemController'
})
.when('/recipes/deleteItem/:id', {
templateUrl: 'partials/deleteItem.html',
controller: 'recipesDeleteItemController'
});
为配方步骤和配方项添加新的Angular控制器
在recipesController.js中注入步骤和项目控制器。
angular
.module('masterChefApp')
.controller('recipesController', recipesController)
.controller('recipesAddController', recipesAddController)
.controller('recipesEditController', recipesEditController)
.controller('recipesDeleteController', recipesDeleteController)
.controller('recipesAddStepController', recipesAddStepController)
.controller('recipesEditStepController', recipesEditStepController)
.controller('recipesDeleteStepController', recipesDeleteStepController)
.controller('recipesAddItemController', recipesAddItemController)
.controller('recipesEditItemController', recipesEditItemController)
.controller('recipesDeleteItemController', recipesDeleteItemController);
recipesAddStepController
创建或提供允许某人向应用程序添加配方步骤的功能。添加配方步骤时,我们需要父配方ID。我们将使用&routeParams
服务来创建配方步骤。从route参数获取配方ID。
recipesAddStepController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesAddStepController($scope, Recipe, $location, $routeParams) {
$scope.recipeStep = new Recipe();
$scope.recipeStep.parentId = $routeParams.id;
$scope.addRecipeStep = function () {
$scope.recipeStep.$saveRecipeStep(function () {
$location.path('/');
});
};
}
recipesEditStepController
创建或提供允许某人将配方步骤更新为应用程序的功能。我们将使用&routeParams
服务来更新配方步骤。从route参数获取配方步骤的ID。
recipesEditStepController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesEditStepController($scope, Recipe, $location, $routeParams) {
$scope.recipeStep = Recipe.getRecipeStep({ id: $routeParams.id });
$scope.editRecipeStep = function () {
$scope.recipeStep.$saveRecipeStep(function () {
$location.path('/');
});
};
}
recipesDeleteStepController
使用$routeParams
获取ID并检索特定的配方步骤。然后提供此函数删除配方步骤到应用程序。
recipesDeleteStepController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesDeleteStepController($scope, Recipe, $location, $routeParams) {
$scope.recipeStep = Recipe.getRecipeStep({ id: $routeParams.id });
$scope.deleteRecipeStep = function () {
$scope.recipeStep.$removeRecipeStep({ id: $scope.recipeStep.id }, function () {
$location.path('/');
});
};
}
recipesAddItemController
创建或提供允许某人向应用程序添加配方项目的功能。添加配方项目时,我们需要父配方步骤ID。我们将使用&routeParams
服务获取要创建的配方项目。从route参数获取配方步骤的ID。
recipesAddItemController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesAddItemController($scope, Recipe, $location, $routeParams) {
$scope.recipeItem = new Recipe();
$scope.recipeItem.parentId = $routeParams.id;
$scope.addRecipeItem = function () {
$scope.recipeItem.$saveRecipeItem(function () {
$location.path('/');
});
};
}
recipesEditItemController
创建或提供允许某人将配方项目更新到应用程序的功能。我们将通过使用&routeParams
服务来更新配方项目。通过路由参数获取配方项的ID。
recipesEditItemController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesEditItemController($scope, Recipe, $location, $routeParams) {
$scope.recipeItem = Recipe.getRecipeItem({ id: $routeParams.id });
$scope.editRecipeItem = function () {
$scope.recipeItem.$saveRecipeItem(function () {
$location.path('/');
});
};
}
recipesDeleteItemController
使用$routeParams
获取ID并检索特定的配方项目。然后提供此函数删除配方项到应用程序。
recipesDeleteItemController.$inject = ['$scope', 'Recipe', '$location', '$routeParams'];
function recipesDeleteItemController($scope, Recipe, $location, $routeParams) {
$scope.recipeItem = Recipe.getRecipeItem({ id: $routeParams.id });
$scope.deleteRecipeItem = function () {
$scope.recipeItem.$removeRecipeItem({ id: $scope.recipeItem.id }, function () {
$location.path('/');
});
};
}
添加配方步骤和配方项目的所有模板
现在,我们需要为配方步骤和配方项目创建所有模板。在partials文件夹中创建“addStep.html”,“editStep.html”,“deleteStep.html”,“addItem.html”,“editItem.html”和“deleteItem.html”。
1)配方步骤模板
在addStep.html中,使用ng-submit将数据发布到服务器。当用户按下“保存”按钮时,调用一个范围函数addRecipeStep
,该范围函数将在我们控制器的后台将此配方步骤对象提交给服务器。
<h1>Add a new recipe step</h1>
<div class="container-fluid">
<form ng-submit="addRecipeStep()">
<div class="row">
<div class="form-group col-xs-1">
<label for="stepNo">Step No.</label>
<input ng-model="recipeStep.stepNo" name="stepNo" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-8">
<label for="instructions">Instructions</label>
<input ng-model="recipeStep.instructions" name="instructions" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
editStep.html更新现有的配方步骤。使用ng-model
指令将输入字段绑定到范围变量(对象recipeStep
)。此外,在步骤编辑控制器上,还有一个范围函数——editRecipeStep
。
<h1>Edit Recipe Step</h1>
<div class="container-fluid">
<form ng-submit="editRecipeStep()">
<div class="row">
<div class="form-group col-xs-1">
<label for="stepNo">Step No.</label>
<input ng-model="recipeStep.stepNo" name="stepNo" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-8">
<label for="instructions">Instructions</label>
<input ng-model="recipeStep.instructions" name="instructions" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
在deleteStep.html中,我们将提供一个段落进行确认。我们将提供一个按钮,它调用范围函数——deleteRecipeStep
。它将向服务器提交删除特定配方步骤的请求。
<div class="alert alert-warning">
<p>Do you really want to delete this recipe step?</p>
<p> {{recipeStep.stepNo}} - {{recipeStep.instructions}}</p>
</div>
<button ng-click="deleteRecipeStep()" class="btn btn-danger">Yes</button>
<a href="/" class="btn btn-default">No</a>
2)食谱项目模板
在addItem.html中,使用ng-submit将数据发布到服务器。当用户按下“保存”按钮时,调用一个范围函数addRecipeItem
,该范围函数将在我们控制器的后台将此配方项目对象提交给服务器。
<h1>Add a new recipe item</h1>
<div class="container-fluid">
<form ng-submit="addRecipeItem()">
<div class="row">
<div class="form-group col-xs-4">
<label for="name">Name</label>
<input ng-model="recipeItem.name" name="name" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-4">
<label for="quantity">Quantity</label>
<input ng-model="recipeItem.quantity" name="quantity" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-4">
<label for="measurementUnit">Measurement Unit</label>
<input ng-model="recipeItem.measurementUnit" name="measurementUnit" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
editItem.html更新现有的配方项目。使用ng-model
指令将输入字段绑定到范围变量(对象recipeStep
)。此外,在项目编辑控制器上,还有一个范围函数——editRecipeItem
。
<h1>Edit Recipe Item</h1>
<div class="container-fluid">
<form ng-submit="editRecipeItem()">
<div class="row">
<div class="form-group col-xs-4">
<label for="name">Name</label>
<input ng-model="recipeItem.name" name="name" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-4">
<label for="quantity"></label>
<input ng-model="recipeItem.quantity" name="quantity" type="text" class="form-control" />
</div>
</div>
<div class="row">
<div class="form-group col-md-4 col-xs-4">
<label for="measurementUnit"></label>
<input ng-model="recipeItem.measurementUnit" name="measurementUnit" type="text" class="form-control" />
</div>
</div>
<div class="row">
<button type="submit" class="btn btn-primary">Save</button>
<a href="/" class="btn btn-default">Cancel</a>
</div>
</form>
</div>
在deleteItem.html中,我们将提供一个段落进行确认。我们将提供一个调用范围函数——deleteRecipeItem
的按钮。它将向服务器提交删除特定配方项目的请求。
<div class="alert alert-warning">
<p>Do you really want to delete this recipe item?</p>
<p> {{recipeItem.name}} {{recipeItem.quantity}} {{recipeItem.measurementUnit}}</p>
</div>
<button ng-click="deleteRecipeItem()" class="btn btn-danger">Yes</button>
<a href="/" class="btn btn-default">No</a>
一切都完成了。现在,您可以创建、更新或删除配方。您成为真正的大厨。不只是一个只遵循别人食谱的厨师。
IE缓存问题
最后,我想谈一谈IE上发生的缓存问题。如果我们将IIS Express更改为IE使用,则在添加新配方“烤鸭”后,您将看不到我立即添加的新配方。插入不正确吗?去检查数据库,新配方在那里。看起来,当返回列表时,AngularJS根本没有向服务器发送http get请求,而只是从缓存中获取结果。这就是为什么不弹出新更新的原因。我们可以通过httpProvider
解决此问题。在AngularJS应用程序配置函数中注入httpProvider
。然后在http get请求标头中将http默认缓存设置为false,并将If-Modified-Since设置为0。
angular.module('masterChefApp').config(['$routeProvider', '$httpProvider', '$locationProvider', function ($routeProvider, $httpProvider, $locationProvider) {
//disable http cache
$httpProvider.defaults.cache = false;
if (!$httpProvider.defaults.headers.get) {
$httpProvider.defaults.headers.get = {};
}
$httpProvider.defaults.headers.get['If-Modified-Since'] = '0';
//
$routeProvider
.when('/', {
templateUrl: 'partials/recipes.html',
controller: 'recipesController'
})
.when('/recipes/add', {
templateUrl: 'partials/add.html',
controller: 'recipesAddController'
})
.when('/recipes/edit/:id', {
templateUrl: 'partials/edit.html',
controller: 'recipesEditController'
})
.when('/recipes/delete/:id', {
templateUrl: 'partials/delete.html',
controller: 'recipesDeleteController'
})
.when('/recipes/addStep/:id', {
templateUrl: 'partials/addStep.html',
controller: 'recipesAddStepController'
})
.when('/recipes/editStep/:id', {
templateUrl: 'partials/editStep.html',
controller: 'recipesEditStepController'
})
.when('/recipes/deleteStep/:id', {
templateUrl: 'partials/deleteStep.html',
controller: 'recipesDeleteStepController'
})
.when('/recipes/addItem/:id', {
templateUrl: 'partials/addItem.html',
controller: 'recipesAddItemController'
})
.when('/recipes/editItem/:id', {
templateUrl: 'partials/editItem.html',
controller: 'recipesEditItemController'
})
.when('/recipes/deleteItem/:id', {
templateUrl: 'partials/deleteItem.html',
controller: 'recipesDeleteItemController'
});
$locationProvider.html5Mode(true);
}]);
然后,我们再试一次。它像一种魅力。尽管我在Google Chrome浏览器中没有此缓存问题,但我们仍然需要在IE上解决此问题,因为Web应用程序应可在所有浏览器上使用。
结论
在本文中,我介绍了如何使用Angular路由制作SPA CRUD应用程序。我们还讨论了如何在单个服务器端Web API控制器中映射多个URL。相应地,如何在单个客户端Angular资源服务中映射不同的路由。从 Maser Chef第3部分 开始,我们将在Angular2和EntityFramework Core上进行新的冒险。