ASP.NET MVC和Web API中的Angular2 - 第2部分
内容
第1部分:Visual Studio 2017中的Angular2设置,基本CRUD应用程序,第三方模态弹出控件
第2部分:使用Angular2管道进行过滤/搜索,全局错误处理,调试客户端
介绍
在 ASP.NET MVC和Web API - 第1部分中,我们学习了ASP.NET MVC中的基本Angular2设置。在这部分我们将学习:
- 我们如何
UserComponent
通过FirstName,LastName或Gender使用Angular2 来实现搜索/过滤功能来搜索用户pipe
? - 如何通过扩展
ErrorHandler
类来实现全局错误处理? - 如何使用Firefox调试器调试客户端代码?
开始吧
- 首先,浏览 ASP.NET MVC和Web API - 第1部分 ,并下载附件Angular2MVC_Finish.zip文件。将其 提取 到您计算机中所需的位置,然后双击
Angular2MVC.sln
打开解决方案Visual Studio(2015年更新3或2017版)。
- 由于
Angular2MVC
解决方案不需要NuGet
和node_modules
包,请转到Build
菜单并选择Rebuild Solution
,Visual Studio将下载所有列出的.NET包packages.config
和客户端包中提到的package.json
。
- 您将找到
packages
包含所有必需的.NET软件包和node_modules
包含所有客户端软件包的文件夹:
Compile
和run
应用程序,你不应该收到任何错误。- 由于现在已经下载了所有项目依赖关系,因此可以根据用户输入开始实现搜索/过滤数据功能,过滤器实现后的最终输出页面如下:
- 从屏幕截图,您可能已经了解了这个搜索/过滤器功能如何工作,一旦用户开始在搜索文本框中输入文本,数据将在匹配用户输入的文本之后的列表中开始在列表中过滤
First Name
,last Name
并且Gender
领域。由于客户端过滤,这是非常方便快捷的。我喜欢这个功能,因为它避免了丑陋的文本框,其旁边的搜索按钮和服务器端的复杂过滤查询。 - 因此,在接下来的步骤中,我们将学习如何实现此功能。我们将使用Angular2
pipe
来过滤数据,但在跳转到编码之前,让我们来了解一下什么是pipe
如何使用它。 - 虽然Angular2文档对
pipes
这里有很简单和全面的解释,但对于我那种懒惰的人,让我总结一下。pipe
将数据转换为有意义的表示,例如,您12/03/2016
从数据库获取日期并将其转换为Dec 03, 2016
,您可以通过它进行操作pipe
。其他内置的Pipes
是Uppercase
,lowercase
,json
等这是不言自明他们的名字。为什么它被调用pipe
,因为我认为我们使用|
符号将它们应用于变量或值。例如{{Value | uppercase}}
。您可以按照pipes
要求按照|
符号分隔的某个值申请许多{{ birthdate | date | uppercase }}
。您还可以指定参数传递给pipe
由: (colon)
例如日期过滤器可以采取格式参数,{{birthdate | date : ‘MM/dd/yyyy’}}
- Now that we got the basic idea of
pipe
, let’s implement the user search functionality throughpipe
. Just like built inpipes
available in Angular2, we can also implement our own custompipes
, all we need is to implement thePipeTransform
interface and develop custom logic intransform
method that takes two parameters,data
(to be filtered from) andoptional arguments
e.g. user input string to be searched in the data. To read more about custompipes
, click here. - Let’s create the
user
filter pipe, right click on theapp
folder and selectAdd -> New Folde
r, name the folder asfilter
(orpipe
, whatever you prefer):
- Right click on newly created
filter
folder and selectAdd -> TypeScript File
:
user.pipe.ts
在Item name
文本框中输入名称,然后单击OK
按钮:
- 将以下代码粘贴到新添加的
user.pipe.ts
文件中:
隐藏 复制代码
import { PipeTransform, Pipe } from '@angular/core'; import { IUser } from '../Model/user'; @Pipe({ name: 'userFilter' }) export class UserFilterPipe implements PipeTransform { transform(value: IUser[], filter: string): IUser[] { filter = filter ? filter.toLocaleLowerCase() : null; return filter ? value.filter((app: IUser) => app.FirstName != null && app.FirstName.toLocaleLowerCase().indexOf(filter) != -1 || app.LastName != null && app.LastName.toLocaleLowerCase().indexOf(filter) != -1 || app.Gender != null && app.Gender.toLocaleLowerCase().indexOf(filter) != -1 ) : value; } }
- 让我们明白我们刚加入的内容
user.pipe.ts:
- 在第一行中,我们正在导入
PipeTransform
和Pipe
实现的接口以实现过滤功能。 - 在第二行中,我们导入
IUser
我们在第一部分中创建的接口来保存用户列表。在这里,我们还使用它来保存作为过滤源数据的用户列表。 - 在下一行中,我们将指定我们将使用管道的管道
selector
/名称userFilter
(您将在以后的步骤中找到如何)。 - 接下来,我们正在创建实现
UserFilterPipe
接口的类PipeTransform
(实现接口意味着为接口中提到的所有方法提供机构)。 - 右键单击
PipeTransform
并选择选项Go To Definition
:
- 在第一行中,我们正在导入
- 您将登陆到
pipe_transform_d.ts
文件,在那里您将找到一个很好的简要说明如何使用管道与transform
我们必须实现的示例和方法:
- 您将登陆到
- 所以让我们回到
user.pipe.ts
哪里可以看到我们有transform
第一个参数的方法作为IUser
数组,第二个被命名为filter
在IUser
数组中要搜索的输入字符串。 - 在
transform
方法中,第一行只是检查过滤器是否不null
。 - The next statement is the actual implementation of search, if you are C# developer, you can compare it to the
LINQ to Object
. We are calling Array’sfilter
method, checking through conditional operator that if any ofIUser
member (FirstName
,LastName
orGender
) is matching with user input search string and if YES, returning the filtered result.toLocaleLowerCase
method is converting string to lower case, to read more about it, click here. If there is no matching record in User list, we are returning the all rows.
- 所以让我们回到
- Now that we have our filter ready, let’s add it to the
AppModule
to use it in our application, double click onapp.module.ts
file inapp
folder to edit it:
- Update the
AppModule
according to the following screenshot:
- 我们在声明部分加上
UserFilterPipe
声明的import
引用。只是为了修改,declaration
部分中的组件相互认识,这意味着我们可以UserFilterPipe
在UserComponent
(或任何其他组件)中使用,而不添加引用UserComponent
本身。我们可以宣布components
,pipes
etc.indeclaration
部分。 - 所以,我们的用户过滤器/搜索功能已经准备就绪,下一步是使用它,
UserComponent
而不是直接使用它UserComponent
,让我们创建SearchComponent
所有组件可以共享的共享,这将有助于我们了解- 之间的相互作用
parent
(UserComponent)和child
(的SearchComponent)分量。 - 如何通过输入参数发送
@Input
并通过@Output
别名获取值 。
- 之间的相互作用
- 右键单击
Shared
主app
文件夹中的文件夹,然后选择Add -> TypeScript File
:
- 输入
Item name
assearch.component.ts
和点击OK
按钮:
- 复制
search.component.ts
文件中的以下代码,让我们一步一步了解:
隐藏 收缩 复制代码
import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'search-list', template: `<div class="form-inline"> <div class="form-group"> <label><h3>{{title}}</h3></label> </div> <div class="form-group"> <div class="col-lg-12"> <input class="input-lg" placeholder="Enter any text to filter" (paste)="getPasteData($event)" (keyup)="getEachChar($event.target.value)" type="text" [(ngModel)]="listFilter" /><img src="../../images/cross.png" class="cross-btn" (click)="clearFilter()" *ngIf="listFilter"/> </div> </div> <div class="form-group"> <div *ngIf='listFilter'> <div class="h3 text-muted">Filter by: {{listFilter}}</div> </div> </div> </div> ` }) export class SearchComponent { listFilter: string; @Input() title: string; @Output() change: EventEmitter<string> = new EventEmitter<string>(); getEachChar(value: any) { this.change.emit(value); } clearFilter() { this.listFilter = null; this.change.emit(null); } getPasteData(value: any) { let pastedVal = value.clipboardData.getData('text/plain'); this.change.emit(pastedVal); value.preventDefault(); } }
- 第一行我们是导入
Input
,Output
接口和EventEmitter
类。Input
和Output
接口是不言自明的,从UserComponent
(在我们的例子中,来自用户的搜索字符串)的输入参数,Output
是将值发送回来,SearchComponent
但没有什么有趣的,输出是通过使用EventEmitter
类的事件发回的。这将在进一步的步骤中变得更加清楚。 - In next line, we are providing the Component metadata, i.e.
selector
(tag name through which we will useSearchComponent
inUserComponent
e.g.<search-list></search-list>
).template
is the HTML part of component. You can also put it in separate HTML file and specify thetemplateUrl
property instead but since this is quite slim, I would prefer to have it in the same file. - In
SearchComponent
class, we are declaring one local variablelistFilter
that is search string we will use to display here<div class="h3 text-muted">Filter by: {{listFilter}}</div>
. That is only for cosmetic purpose to show what we are searching. - Second variable
title
is with@Input
decorator, we will send search textbox title from UserComponent. Third variablechange
is with@Output
decorator and ofEventEmitter
type. This is how we send data back to parent component.change EventEmitter<string>
means change is an event that parent component needs to subscribe and will get string argument type. We will explicitly callemit
function (i.e.change.emit(“test”)
) to send the value back to the parent component. getEachChar(value: any)
: this function will be called for every character user will enter in search textbox. We are only callingthis.change.emit(value);
that is sending that character to parent component where it is being sent to theUserFilterPipe
pipe to be filtered from User list. Just for revision, inUserPipeFilter
we are comparing that character withFirstName
,LastName
andGender
and returning only those records where this character(s) exist. So as long the user would be entering characters in Search textbox, data would be filtering on runtime.clearFilter()
: Will clear the filter to reset the User list to default without any filtering.getPasteData(value: any)
: This is little interesting function that will take care if user would copy search string from somewhere and paste it in search textbox to filter the Users list. Throughvalue.clipboardData.getData('text/plain')
we are getting the pasted data and sending it throughchange.emit(value)
function to parent component.- Now, we got some idea about these function, if you jump back to
SearchComponent template
(HTML). We are callinggetEachChar
onkeyup
event that will trigger every time user would type in Search textbox,getPasteData
is being called onpaste
event that will occur when user would paste value in Search textbox,clearFilter
function would be called on clicking the cross image that would only be visible if search textbox would have at least one character.
- 第一行我们是导入
- 所以我们完成了创建
SearchComponent
,希望你有一个想法如何工作,让我们添加它,AppModule
以便我们可以使用它,双击app -> app.module.ts
编辑它:
- 添加以下
import
语句:
隐藏 复制代码
import { SearchComponent } from './Shared/search.component';
- 在声明部分添加SearchComponent以在任何组件中使用它:
隐藏 复制代码
declarations: [AppComponent, UserComponent, HomeComponent, UserFilterPipe, SearchComponent],
- 所以现在我们
SearchComponent
已经准备好使用了,我们在这里使用它UserComponent
。双击app -> Components -> user.component.html
进行编辑:
- 我们将添加
SearchComponent
在用户列表的顶部,因此在Add
按钮顶部添加以下div :
隐藏 复制代码
<div> <search-list [title]='searchTitle' (change)="criteriaChange($event)"></search-list> </div>
- 让我们明白一下,它看起来像普通的HTML,但是
search-list
标签。如果你记得,这是我们在文件中定义的selector
属性。如果您在第1部分中记住,我们了解到,用于将数据从父组件发送到子组件。我们通过我们定义的变量来评价孩子的组件变量的价值。二是事件绑定,我们创建的事件,我们提供的功能中,当发生变化的事件将执行每一次。将保存事件发送的任何值,在我们的例子中,我们发送每个字符用户将进入搜索文本框(参见功能)。SearchComponent
search.component.ts
Property Binding [ ]
title
searchTitle
UserComponent
( )
change
SearchComponent
criteriaChange
UserComponent
$event
change
getEachChar
SearchComponent
- 由于我们
criteriaChange
在事件绑定中指定了函数,search-list
所以我们将其添加到UserComponent
。双击app -> Components -> user.component.ts
进行编辑:
- 添加以下功能
user.component.ts
:
隐藏 复制代码
criteriaChange(value: string): void { if (value != '[object Event]') this.listFilter = value; }
- 您可以看到我们从
change
事件中获取输入参数值(用户在搜索文本框中输入的文本),并将其分配给listFilter
我们将用于pipe
过滤器的变量。让我们继续声明listFilter
变量。添加以下行与其他变量声明语句:
隐藏 复制代码
listFilter: string;
- 到目前为止,我们已经创建了
SearchComponent
一个具有交叉图像按钮的文本框,以便按照用户搜索文本的只读显示来清除搜索。在父母中UserComponent
,我们订阅了change
事件,并在搜索文本框中获取用户输入的每个字符,并将其分配给listFilter
变量,其中它被累积(例如,用户输入字符'a'),将被发送到过滤器,其中所有包含“a”的记录将被过滤后,如果用户将其他任何字符(如'f')'a',那么'a'和'f'都将被发送为“af”进行过滤,并且所有具有“af”组合的记录将被过滤,因此上)。一旦你开始使用它,或者你可以调试它,我将在即将到来的步骤解释)。所以,最后一步是如何根据在搜索文本框中输入的搜索文本来过滤用户列表?<tr *ngFor="let user of users">
因此,从刷新前面的步骤你管知识和更新在app->Components -> user.component.html
到<tr *ngFor="let user of users | userFilter:listFilter">
。userFilter
我们在前面的步骤中创建的过滤器在哪里,是要过滤listFilter
的输入参数。 - 由于我们使用定义的双向数据绑定
[(ngModel)]
的listFilter
变量FormsModule
,我们将其添加到其中AppModule
,以便更新
隐藏 复制代码
import { ReactiveFormsModule } from '@angular/forms';
至
隐藏 复制代码
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; in AppModule.
- 添加
formsModule
在导入部分。
- 在任何浏览器中编译和运行项目(建议使用Firefox或Chrome)。转到
UserManagement
页面,现在可能有很少的记录,你可以继续添加15,20多个。开始输入First Name
,Last Name
或Gender
在搜索文本框中,您将看到运行时过滤的记录:
- 这就是我们的过滤。
- 接下来,我们将了解Angular2中的错误处理,我将保持简单,不会在每个错误类型中出现,但是让您知道如何对每个错误类型都有自定义错误。有关Angular2
ErrorHandler
类的快速参考,请点击此处。 - 对于自定义错误处理程序类,我们可以扩展
ErrorHandler
hasconstructor
和handleError
使用error
参数的方法。误差参数具有完整的错误的信息,例如status
,error code
,error text
等等取决于错误类型(HTTP,应用等)。这真的有助于自定义错误消息。ErrorHandler
处理任何类型的错误,例如未声明的变量/函数,任何数据异常或HTTP错误。此外ErrorHandler
, - 我们将从中删除错误处理代码
UserService
,UserComponent
以便我们可以捕获ErrorHandler
类中的所有错误,然后我们使用Firefox检查错误debugger
。所以,让我们开始吧。 - 首先,我们创建自定义错误处理程序类。右键单击
app -> Shared
文件夹,然后选择Add -> TypeScript File
:
- 输入名称
errorhandler.ts
并点击OK
按钮:
- 将以下代码粘贴到新创建的文件中:
隐藏 复制代码
import { ErrorHandler } from '@angular/core'; export default class AppErrorHandler extends ErrorHandler { constructor() { // We rethrow exceptions, so operations like 'bootstrap' will result in an error // when an error happens. If we do not rethrow, bootstrap will always succeed. super(true); } handleError(error: any) { debugger; alert(error); super.handleError(error); } }
- 代码是非常不言自明的评论,即为什么我们正在调用
super(true)
构造函数。AppErrorHandler
是我们的定制类,它正在扩展Angular2ErrorHandler
类并实现该handleError
功能。在handleError,
我把调试器显示出来会有什么错误,以及如何自定义它。我们通过简单的JavaScript警报功能显示错误消息。 - 首先让我们看看
HTTP
错误。假设我们在加载数据库中的所有用户之前都有认证逻辑,并且请求以某种方式未被认证,我们not authorized (401)
将从ASP.NET Web API向Angular2 发送错误。让我们得到这个错误AppErrorHandler
并检查它。 - 接下来,添加
AppErrorHandler
inAppModule
来捕获所有错误。添加以下import
语句:
AppErrorHandler
从... 导入'./Shared/errorhandler';
- 更新提供程序部分以具有
ErrorHandler
:
隐藏 复制代码
providers: [{ provide: ErrorHandler, useClass: AppErrorHandler },{ provide: APP_BASE_HREF, useValue: '/' }, UserService]
- 我们告诉我们的模块使用我们的自定义错误处理程序来处理任何错误。不要忘记在
ErrorHandler
语句中添加类@angular2/core
import
引用:
- 我们来评论
UserComponent.ts
文件app -> Components
夹中的错误处理。双击进行编辑。转到LoadUsers
功能并更新它如下:
隐藏 复制代码
LoadUsers(): void { this.indLoading = true; this._userService.get(Global.BASE_USER_ENDPOINT) .subscribe(users => { this.users = users; this.indLoading = false; } //,error => this.msg = <any>error ); }
- 您可以看到我已经注释掉了在
msg
屏幕底部显示的变量中保存的错误语句。 - 接下来我们来评论
user.service.ts
文件中的错误处理,在文件夹中找到它app -> Service
并双击它进行编辑。更新get方法如下,我评论了catch
语句:
隐藏 复制代码
get(url: string): Observable<any> { return this._http.get(url) .map((response: Response) => <any>response.json()); // .do(data => console.log("All: " + JSON.stringify(data))) // .catch(this.handleError); }
- 现在我们的客户端代码已经准备好捕获HTTP异常,我们来添加
unauthorized
异常代码UserAPIController
(基本上我们将添加它BaseAPIController
并调用它UserAPIController
)。 - 转到
Controllers
文件夹并双击BaseAPIController.cs
进行编辑:
- 添加以下
ErrorJson
函数实际上是ToJson
方法的副本,但只是使用Unauthorized
状态代码(我刚刚创建了样例,应该为HTTP调用创建更专业的错误处理代码):
隐藏 复制代码
protected HttpResponseMessage ErrorJson(dynamic obj) { var response = Request.CreateResponse(HttpStatusCode.Unauthorized); response.Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); return response; }
- 由于我迄今没有任何身份验证逻辑,所以在
UserAPIController
我只是更新Get()
方法如下,只是替换了这个ToJson
功能ErrorJson
,现在API将永远抛出Unauthorized
我们将尝试加载用户的excleion:
隐藏 复制代码
public HttpResponseMessage Get() { return ErrorJson(UserDB.TblUsers.AsEnumerable()); }
- 编译并运行项目,转到
User Management
页面。几分钟后,您会看到如下所示的丑恶警报信息:
- 很好,所以我们的测试环境成功创建。我们将这个错误从
UserAPI
Get()
方法发送到我们的定制中被捕获的客户端AppErrorHandler
。 - 我们来调试错误,在
errorhandler.ts
文件中,点击旁边的灰色条debugger
来设置断点:
- 在Firefox中运行应用程序,按
Ctrl+Shift+S
或单击打开menu button => Developer => Debugger
:
- 你应该结束以下屏幕:
- 转到
User Management
页面,稍后,您将看到执行停止在debugger
:
- 鼠标悬停在
error
上方,您会看到所有参数错误:
- 由于这是HTTP错误,你可以看到的
HTTPStatusCode
是401 (Unauthorized request)
,主体部分仍然有一定你永远也不会发回的数据,而不是你可以在这里发送用户友好的错误消息。 - 通过考虑这些错误参数,我们可以通过检查状态代码来扩展我们的错误处理。我们开始做吧。
- 更新
handleError
在以下errorhandler.ts
文件:
隐藏 复制代码
handleError(error: any) { debugger; if (error.status == '401') alert("You are not logged in, please log in and come back!") else alert(error); super.handleError(error); }
Compile
与run
应用程序再次,转到User Management
页一次。您现在将看到以下用户友好的错误消息:
- Firefox
debugger
是调试客户端代码的好工具,花一些时间来探索更多有用的功能。你可以步骤next line
,into the function
或step out
通过突出显示的按钮:
- 接下来我们来弄清我们的应用程序,并通过Firefox调试检查错误变量。双击
app => Components => home.component.ts
进行编辑。 - 在模板部分输入以下html:
隐藏 复制代码
<button class="btn btn-primary" (click)="IdontExist()">ErrorButton</button>
- 最终模板应如下:
- 我添加了一个带有
IdontExist()
不存在的调用函数的click事件的按钮HomeComponent
。 - 让我们运行应用程序,然后运行调试器,你会
ErrorButton
在屏幕中间看到愚蠢:
- 点击
ErrorButton
,再次看到执行停止在debugger
(断点),鼠标悬停错误,浏览参数弹出或点击watch
链接在底部放错误变量在右侧Variables
部分:
- 您可以看到这一堆新的信息,展开该
originalError
部分,您将看到实际的错误:
- 您可以看到非常详细的信息来挖掘复杂的错误。
- 按
Resume
左侧的按钮继续执行:
- 您会看到简短的错误信息:
- 调试是在客户端获取完整信息的好工具。
Thanks & Best Regards!
Javi Zhu 朱佳辉
Mobile: 15900467108
Email: Javi.zhu@outlook.com