Angular2 in ASP.NET MVC & Web API - 第 2 部分






4.94/5 (27投票s)
在本部分中,我们将增强用户管理应用程序,以具备搜索/过滤、全局错误处理和调试功能。
系列文章
- 第 1 部分:在 Visual Studio 2017 中设置 Angular2,基本的 CRUD 应用程序,第三方模态弹出窗口控件
- 第 2 部分:使用 Angular2 管道进行过滤/搜索,全局错误处理,客户端调试
- 第 3 部分:Angular 2 到 Angular 4,包含 Angular Material 组件
- 第 4 部分:Angular 4 数据网格,支持导出到 Excel、排序和过滤
引言
在 ASP.NET MVC & Web API - 第 1 部分 中,我们学习了 ASP.NET MVC 中 Angular2 的基本设置。在本部分中,我们将学习
- 如何使用 Angular2 的
pipe
在UserComponent
中实现搜索/过滤功能,通过FirstName
、LastName
或Gender
搜索用户? - 如何通过扩展
ErrorHandler
类来实现全局错误处理? - 如何使用 Firefox 调试器调试客户端代码?
开始吧
- 首先,请查看 ASP.NET MVC & Web API - 第 1 部分 并下载附带的 Angular2MVC_Finish.zip 文件。将其解压到您计算机上的任意位置,然后双击 Angular2MVC.sln 在 Visual Studio (2015 Update 3 或 2017) 中打开解决方案。
- 由于
Angular2MVC
解决方案没有所需的NuGet
和node_modules
包,请转到 **Build** 菜单,然后选择 **Rebuild Solution**。Visual Studio 将下载 packages.config 中列出的所有 .NET 包以及 package.json 中提到的客户端包。 - 您会发现 packages 文件夹包含所有必需的 .NET 包,以及 node_modules 文件夹包含所有客户端包。
- 编译并运行应用程序,您应该不会收到任何错误。
- 既然现在已经下载了所有项目依赖项,让我们开始实现搜索/过滤功能,以响应用户输入,过滤实现后的最终页面将如下所示:
- 从截图来看,您可能已经对搜索/过滤功能的工作方式有了一些了解。一旦用户在 **Search** 文本框中开始输入文本,数据就会在列表中实时过滤,匹配用户输入的文本与 **First Name**、**last Name** 和 **Gender** 字段。这因为是客户端过滤,所以非常方便快捷。我喜欢这个功能,因为它避免了难看的带搜索按钮的文本框和复杂的服务器端过滤查询。
- 因此,在接下来的步骤中,我们将学习如何实现此功能。我们将使用 Angular2 的
pipe
来过滤数据,但在开始编写代码之前,让我们先了解一下pipe
是什么以及如何使用它。 - 虽然 Angular2 文档对此有非常简单全面的解释 在此,但对于我这种懒人来说,让我为您总结一下。
pipe
将数据转换为有意义的表示形式,例如,您从数据库获取日期12/03/2016
,并希望将其转换为Dec 03, 2016
。您可以通过pipe
来实现。其他内置Pipes
包括Uppercase
、lowercase
、json
等,这些名称一看就明白。为什么叫pipe
,我认为是因为我们使用|
符号将其应用于变量或值。例如:{{Value | uppercase}}
。您可以根据需要将任意数量的pipes
应用于特定值,用|
分隔,例如:{{ birthdate | date | uppercase }}
。您也可以通过: (冒号)
为pipe
指定参数,例如,日期过滤器可以接受格式参数:{{birthdate | date : ‘MM/dd/yyyy’}}
。 - 现在我们对
pipe
有了基本了解,让我们通过pipe
实现用户搜索功能。就像 Angular2 中可用的内置pipes
一样,我们也可以实现自己的自定义pipes
。我们只需要实现PipeTransform
接口,并在transform
方法中开发自定义逻辑,该方法接受两个参数:data
(要过滤的数据源)和optional arguments
(例如,要在数据中搜索的用户输入string
)。要了解更多关于自定义pipes
的信息,请单击 此处。 - 让我们创建一个
user
过滤 pipe。右键单击app
文件夹,然后选择Add -> New Folde
r,将文件夹命名为filter
(或pipe
,随您喜欢)。 - 右键单击新创建的 filter 文件夹,然后选择 **Add -> TypeScript File**
- 在 **Item name** 文本框中输入 user.pipe.ts,然后单击 **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
接口,用于保存用户列表。在这里,我们也使用它来保存用户列表,这是过滤的源数据。 - 下一行,我们通过
userFilter
指定了 pipe 的selector
/名称,我们将使用该名称来使用 pipe(您将在后续步骤中找到具体方法)。 - 接下来,我们创建了
UserFilterPipe
类,该类实现了PipeTransform
接口(实现接口意味着为接口中提到的所有方法提供主体)。 - 右键单击
PipeTransform
,然后选择 **Go To Definition** 选项。 - 您将被带到 pipe_transform_d.ts 文件,在那里您将找到关于如何使用 pipe 的简洁描述和示例,以及我们必须实现的
transform
方法。 - 所以,让我们回到 user.pipe.ts。可以看到我们有一个
transform
方法,第一个参数是IUser
数组,第二个参数名为filter
,它是要搜索的输入string
,用于在IUser
数组中进行搜索。 - 在
transform
方法中,第一行只是检查 filter 是否不为null
。 - 下一条语句是实际的搜索实现。如果您是 C# 开发人员,可以将其与
LINQ to Object
进行比较。我们调用 Array 的filter
方法,通过 条件运算符 检查IUser
的任何成员(FirstName
、LastName
或Gender
)是否与用户输入的搜索string
匹配,如果匹配,则返回过滤后的结果。toLocaleLowerCase
方法将string
转换为小写。要了解更多信息,请单击 此处。如果用户列表中没有匹配的记录,我们将返回所有行。
- 第一行,我们导入了
- 现在我们的过滤器已准备就绪,让我们将其添加到
AppModule
以便在应用程序中使用它。双击 app 文件夹中的 app.module.ts 文件进行编辑。 - 根据以下屏幕截图更新
AppModule
。 - 我们通过
import
语句和声明部分添加了UserFilterPipe
的引用。仅供回顾,declaration
部分中的组件可以相互识别,这意味着我们可以在UserComponent
(或任何其他组件)中使用UserFilterPipe
,而无需在UserComponent
本身中添加引用。我们可以在declaration
部分声明components
、pipes
等。 - 因此,我们的用户过滤/搜索功能已准备就绪。下一步是在
UserComponent
中使用它。但我们不直接在UserComponent
中使用它,而是创建一个共享的SearchComponent
,所有组件都可以共享。这将帮助我们理解父级
(UserComponent
)和子级
(SearchComponent
)组件之间的交互。- 如何通过
@Input
发送输入参数,并通过@Output
别名获取值。
- 右键单击主 app 文件夹中的 Shared 文件夹,然后选择 **Add -> TypeScript File**
- 将 **Item name** 输入为 search.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
类的事件发送回的。在后续步骤中,这一点将更加清晰。 - 下一行,我们提供了
Component
元数据,即selector
(我们将使用SearchComponent
的标签名称在UserComponent
中,例如<search-list></search-list>
)。template
是组件的 HTML 部分。您也可以将其放在单独的 HTML 文件中,并指定templateUrl
属性,但由于它非常精简,我倾向于将其放在同一个文件中。 - 在
SearchComponent
类中,我们声明了一个局部变量listFilter
,它是我们将在<div class="h3 text-muted">Filter by: {{listFilter}}</div>
中显示的搜索string
。这仅用于化妆目的,显示我们正在搜索的内容。 - 第二个变量
title
带有@Input
装饰器。我们将从UserComponent
发送搜索textbox
的标题。第三个变量change
带有@Output
装饰器,类型为EventEmitter
。这是我们将数据发送回父组件的方式。change EventEmitter<string>
表示 change 是父组件需要订阅的事件,它将接收string
参数类型。我们将显式调用emit
函数(即change.emit(“test”)
)将值发送回父组件。 getEachChar(value: any)
:每当用户在搜索文本框中输入字符时,都会调用此函数。我们只调用this.change.emit(value);
,它会将该字符发送到父组件,然后发送到UserFilterPipe
pipe,以从 User 列表中过滤。仅供回顾,在UserPipeFilter
中,我们将该字符与FirstName
、LastName
和Gender
进行比较,并仅返回包含该字符(s) 的记录。因此,只要用户在搜索文本框中输入字符,数据就会实时过滤。clearFilter()
:将清除过滤器,将 User 列表重置为默认状态,不进行任何过滤。getPasteData(value: any)
:这是一个稍微有趣的功能,它将处理用户从其他地方复制搜索字符串并将其粘贴到搜索文本框中以过滤 Users 列表的情况。通过value.clipboardData.getData('text/plain')
,我们获取粘贴的数据,并通过change.emit(value)
函数将其发送到父组件。- 现在我们对这些函数有了一些了解。如果您回到
SearchComponent template
(HTML),我们在keyup
事件上调用getEachChar
,当用户在搜索文本框中键入时,它将触发。getPasteData
在paste
事件上被调用,当用户在搜索文本框中粘贴值时会发生。clearFilter
函数将在单击交叉图像时调用,该图像仅在搜索文本框中至少有一个字符时可见。
- 第一行,我们导入了
- 因此,我们完成了
SearchComponent
的创建,并希望您了解其工作原理。让我们将其添加到AppModule
中,以便我们可以使用它。双击 app -> app.module.ts 进行编辑。 - 添加以下
import
语句。import { SearchComponent } from './Shared/search.component';
- 在 declarations 部分添加
SearchComponent
以便在任何组件中使用它。declarations: [AppComponent, UserComponent, HomeComponent, UserFilterPipe, SearchComponent],
- 现在我们的
SearchComponent
已准备好使用。让我们在UserComponent
中使用它。双击 **app -> Components ->** user.component.html 进行编辑。 - 我们将
SearchComponent
添加到 User 列表的顶部。因此,在 **Add** 按钮的顶部附加以下div
。<div> <search-list [title]='searchTitle' (change)="criteriaChange($event)"></search-list> </div>
- 让我们来理解一下。它看起来像普通的 HTML,但带有
search-list
标签。如果您还记得,这就是我们在 search.component.ts 文件中为SearchComponent
定义的selector
属性。如果您还记得在第 1 部分中,我们学习了Property Binding [ ]
,它用于将数据从父组件发送到子组件。我们通过UserComponent
中定义的searchTitle
变量将值赋给子组件的title
变量。第二个是事件绑定( )
,我们在SearchComponent
中创建了change
事件,并且我们在UserComponent
中提供了criteriaChange
函数,当 change 事件发生时,该函数将执行。$event
将包含change
事件发送的任何值。在本例中,我们发送用户在搜索文本框中输入的每个字符(请参阅SearchComponent
中的getEachChar
函数)。这就是我们如何从子组件获取值。 - 由于我们在
search-list
的事件绑定中指定了criteriaChange
函数,所以让我们将其添加到UserComponent
中。双击 **app -> Components ->** user.component.ts 进行编辑。 - 在 user.component.ts 中添加以下函数。
criteriaChange(value: string): void { if (value != '[object Event]') this.listFilter = value; }
- 您可以看到我们正在获取输入参数值(用户在搜索文本框中输入的文本)来自
change
事件,并将其分配给我们将用于pipe
过滤的listFilter
变量。让我们继续声明listFilter
变量。将以下行与其他变量声明语句一起添加。listFilter: string;
- 到目前为止,我们已经创建了
SearchComponent
,它有一个textbox
,旁边有一个带有交叉图像的按钮用于清除搜索,并且可以只读显示用户搜索文本。在父级UserComponent
中,我们订阅了change
事件,并在搜索文本框中获取用户输入的每个字符,并将其分配给listFilter
变量,在那里它会被累加(例如,用户输入字符 'a
',它将被发送到过滤器,其中所有包含 'a
' 的记录都将被过滤;在 'a
' 之后,如果用户输入其他字符,如 'f
',那么 'a
' 和 'f
' 都将作为 "af
" 发送进行过滤,依此类推)。一旦您开始使用它,或者可以通过我将在接下来的步骤中解释的调试来理解这一点)。因此,最后一步是如何根据搜索文本框中输入的搜索文本过滤用户列表?因此,请回顾前面步骤中的 pipe 知识,并将 app->Components -> user.component.html 中的<tr *ngFor="let user of users">
更新为<tr *ngFor="let user of users | userFilter:listFilter">
。其中userFilter
是我们早期创建的过滤器,listFilter
是过滤的输入参数。 - 由于我们为
listFilter
变量使用了[(ngModel)]
进行双向数据绑定,而这在FormsModule
中定义,所以让我们将其添加到AppModule
中。更新import { ReactiveFormsModule } from '@angular/forms';
to
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; in AppModule.
- 在 imports 部分添加
formsModule
。 - 在任何浏览器(推荐 Firefox 或 Chrome)中编译并运行项目。转到
UserManagement
页面。目前可能只有少量记录,您可以添加 15、20 条更多记录。开始在 **Search** 文本框中输入First Name
、Last Name
或Gender
,您将看到记录在运行时被过滤。 - 这就是我们的过滤功能。
- 接下来,我们将学习 Angular2 中的错误处理。我将保持简单,不深入探讨每种错误类型,但会告诉您如何为每种错误类型自定义错误。有关 Angular2
ErrorHandler
类的快速参考,请单击 此处。 - 对于自定义错误处理程序类,我们可以扩展
ErrorHandler
类,该类具有constructor
和handleError
方法,并带有error
参数。error 参数包含完整的错误信息,例如status
、error code
、error text
等,具体取决于错误类型(HTTP、应用程序等)。这对于自定义错误消息非常有帮助。ErrorHandler
可以处理任何类型的错误,例如未声明的变量/函数、任何数据异常或 HTTP 错误。除了ErrorHandler
,我还会解释如何使用 Firefox 浏览器调试代码(您也可以在 Chrome 或 Internet Explorer 中进行调试)。 - 我们将注释掉
UserService
和UserComponent
中的错误处理代码,以便我们可以捕获ErrorHandler
类中的所有错误,然后使用 Firefoxdebugger
检查错误。那么,让我们开始吧。 - 首先,让我们创建自定义错误处理程序类。右键单击 **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
是我们的自定义类,它扩展了 Angular2 的ErrorHandler
类并实现了handleError
函数。在handleError
中,我放了一个 debugger 来向您展示将出现什么错误以及如何自定义它。我们通过简单的 JavaScriptalert
函数显示错误消息。 - 首先,让我们看看
HTTP
错误。假设我们在从数据库加载所有用户之前有身份验证逻辑,并且请求由于某种原因未经验证,我们将从 ASP.NET Web API 发送not authorized (401)
错误到 Angular2。让我们在AppErrorHandler
中获取此错误并进行检查。 - 接下来,将
AppErrorHandler
添加到AppModule
以捕获所有错误。添加以下import
语句。import AppErrorHandler from './Shared/errorhandler';
- 更新 provider 部分以包含
ErrorHandler
。providers: [{ provide: ErrorHandler, useClass: AppErrorHandler }, { provide: APP_BASE_HREF, useValue: '/' }, UserService]
- 我们告诉我们的模块使用自定义错误处理程序来处理任何类型的错误。不要忘记在
@angular2/core
import
语句中添加ErrorHandler
类的引用。 - 让我们注释掉 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 异常。让我们在
UserAPIController
中添加unauthorized
异常代码(基本上,我们将其添加到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
异常。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)
。body 部分仍然包含数据,您肯定永远不会将其发送回,而是可以在此处发送用户友好的错误消息。 - 通过考虑这些错误参数,我们可以通过检查状态代码来扩展我们的错误处理。让我们这样做。
- 在 errorhandler.ts 文件中,按以下方式更新
handleError
。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); }
- **编译**
User Management
页面。您现在将看到以下用户友好的错误消息。 - Firefox
debugger
是一个很棒的客户端代码调试工具,花一些时间探索更多有用的功能。您可以通过高亮显示的按钮 **step to next line**、**step into the function** 或 **step out**。 - 接下来,让我们弄乱我们的应用程序,看看通过 Firefox 调试器在 error 变量中会有什么。双击 **app => Components =>** home.component.ts 进行编辑。
- 在 template 部分输入以下 HTML。
<button class="btn btn-primary" (click)="IdontExist()">ErrorButton</button>
- 最终模板应如下所示。
- 我添加了一个带有 click 事件的按钮,该按钮调用
IdontExist()
函数,该函数在HomeComponent
中不存在。 - 让我们运行应用程序,然后运行调试器。您会在屏幕中间看到一个愚蠢的
ErrorButton
。 - 单击
ErrorButton
,您会再次看到执行在debugger
(断点)处停止。将鼠标悬停在 error 上,浏览弹出窗口中的参数,或单击底部的watch
链接,将 error 变量添加到右侧的Variables
部分。 - 您可以看到这一大堆新信息。展开
originalError
部分,您将看到实际的错误。 - 您可以看到非常详细的信息,用于深入研究复杂的错误。
- 按左侧的 **Resume** 按钮继续执行。
- 您将看到简短的错误消息。
- 调试是获取客户端完整信息的绝佳工具。
历史
- 2017年5月13日:创建