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调试器调试客户端代码?

开始吧

  1. 首先,浏览  ASP.NET MVC和Web API - 第1部分  ,并下载附件Angular2MVC_Finish.zip文件。将其  提取  到您计算机中所需的位置,然后双击Angular2MVC.sln打开解决方案Visual Studio(2015年更新3或2017版)。
  1. 由于Angular2MVC 解决方案不需要NuGet node_modules包,请转到Build 菜单并选择Rebuild Solution,Visual Studio将下载所有列出的.NET包packages.config和客户端包中提到的package.json
  1. 您将找到packages 包含所有必需的.NET软件包和node_modules包含所有客户端软件包的文件夹:
  1. Compile run 应用程序,你不应该收到任何错误。
  2. 由于现在已经下载了所有项目依赖关系,因此可以根据用户输入开始实现搜索/过滤数据功能,过滤器实现后的最终输出页面如下:
  1. 从屏幕截图,您可能已经了解了这个搜索/过滤器功能如何工作,一旦用户开始在搜索文本框中输入文本,数据将在匹配用户输入的文本之后的列表中开始在列表中过滤First Namelast Name并且Gender 领域。由于客户端过滤,这是非常方便快捷的。我喜欢这个功能,因为它避免了丑陋的文本框,其旁边的搜索按钮和服务器端的复杂过滤查询。
  2. 因此,在接下来的步骤中,我们将学习如何实现此功能。我们将使用Angular2 pipe 来过滤数据,但在跳转到编码之前,让我们来了解一下什么是pipe 如何使用它。
  3. 虽然Angular2文档对pipes 这里有很简单和全面的解释,但对于我那种懒惰的人,让我总结一下。pipe将数据转换为有意义的表示,例如,您12/03/2016从数据库获取日期并将其转换为Dec 03, 2016,您可以通过它进行操作pipe。其他内置的Pipes Uppercaselowercasejson 等这是不言自明他们的名字。为什么它被调用pipe,因为我认为我们使用|符号将它们应用于变量或值。例如 {{Value | uppercase}}。您可以按照pipes  要求按照|符号分隔的某个值申请许多{{ birthdate | date | uppercase }}。您还可以指定参数传递给pipe : (colon) 例如日期过滤器可以采取格式参数,{{birthdate | date : ‘MM/dd/yyyy’}}
  4. Now that we got the basic idea of pipe, let’s implement the user search functionality through pipe. Just like built in pipes available in Angular2, we can also implement our own custom pipes, all we need is to implement the PipeTransform interface and develop custom logic in transform method that takes two parameters, data (to be filtered from) and optional arguments e.g. user input string to be searched in the data. To read more about custom pipes, click here.
  5. Let’s create the user filter pipe, right click on the app folder and select Add -> New Folder, name the folder as filter (or pipe, whatever you prefer):
  1. Right click on newly created filter folder and select Add -> TypeScript File:
  1. user.pipe.tsItem name文本框中输入名称,然后单击OK 按钮:
  1. 将以下代码粘贴到新添加的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; }
}
  1. 让我们明白我们刚加入的内容 user.pipe.ts:
    1. 在第一行中,我们正在导入PipeTransform Pipe 实现的接口以实现过滤功能。
    2. 在第二行中,我们导入IUser 我们在第一部分中创建的接口来保存用户列表。在这里,我们还使用它来保存作为过滤源数据的用户列表。
    3. 在下一行中,我们将指定我们将使用管道的管道selector/名称userFilter (您将在以后的步骤中找到如何)。
    4. 接下来,我们正在创建实现UserFilterPipe 接口的类PipeTransform (实现接口意味着为接口中提到的所有方法提供机构)。
    5. 右键单击PipeTransform 并选择选项Go To Definition
    1. 您将登陆到pipe_transform_d.ts文件,在那里您将找到一个很好的简要说明如何使用管道与transform 我们必须实现的示例和方法:
    1. 所以让我们回到user.pipe.ts哪里可以看到我们有transform 第一个参数的方法作为IUser 数组,第二个被命名为filter IUser 数组中要搜索的输入字符串。
    2. transform 方法中,第一行只是检查过滤器是否不null
    3. 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’s filter method, checking through conditional operator that if any of IUser member (FirstNameLastName or Gender) 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.
  1. Now that we have our filter ready, let’s add it to the AppModule to use it in our application, double click on app.module.ts file in app folder to edit it:
  1. Update the AppModule according to the following screenshot:
  1. 我们在声明部分加上UserFilterPipe 声明的import 引用。只是为了修改,declaration 部分中的组件相互认识,这意味着我们可以UserFilterPipe UserComponent (或任何其他组件)中使用,而不添加引用UserComponent 本身。我们可以宣布componentspipes etc.in declaration 部分。
  2. 所以,我们的用户过滤器/搜索功能已经准备就绪,下一步是使用它,UserComponent 而不是直接使用它UserComponent,让我们创建SearchComponent 所有组件可以共享的共享,这将有助于我们了解
    1. 之间的相互作用parent (UserComponent)和child (的SearchComponent)分量。
    2. 如何通过输入参数发送@Input并通过@Output别名获取值  。
  3. 右键单击Shared app 文件夹中的文件夹,然后选择Add -> TypeScript File
  1. 输入Item nameas search.component.ts和点击OK 按钮:
  1. 复制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();
}
}
    1. 第一行我们是导入InputOutput 接口和EventEmitter 类。Input Output 接口是不言自明的,从UserComponent (在我们的例子中,来自用户的搜索字符串)的输入参数,Output 是将值发送回来,SearchComponent 但没有什么有趣的,输出是通过使用EventEmitter 类的事件发回的。这将在进一步的步骤中变得更加清楚。
    2. In next line, we are providing the Component metadata, i.e. selector (tag name through which we will use SearchComponent in UserComponent e.g. <search-list></search-list>). template is the HTML part of component. You can also put it in separate HTML file and specify the templateUrl property instead but since this is quite slim, I would prefer to have it in the same file.
    3. In SearchComponent class, we are declaring one local variable listFilter 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.
    4. Second variable title is with @Input decorator, we will send search textbox title from UserComponent. Third variable change is with @Output decorator and of EventEmitter 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 call emit function (i.e. change.emit(“test”)) to send the value back to the parent component.
    5. getEachChar(value: any): this function will be called for every character user will enter in search textbox. We are only calling this.change.emit(value); that is sending that character to parent component where it is being sent to the UserFilterPipe pipe to be filtered from User list. Just for revision, in UserPipeFilter we are comparing that character with FirstNameLastNameand Gender 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.
    6. clearFilter(): Will clear the filter to reset the User list to default without any filtering.
    7. 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. Through value.clipboardData.getData('text/plain') we are getting the pasted data and sending it through change.emit(value) function to parent component.
    8. Now, we got some idea about these function, if you jump back to SearchComponent template(HTML). We are calling getEachChar on keyup event that will trigger every time user would type in Search textbox, getPasteData is being called on paste 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.
  1. 所以我们完成了创建SearchComponent ,希望你有一个想法如何工作,让我们添加它,AppModule 以便我们可以使用它,双击app -> app.module.ts编辑它:
  1. 添加以下import 语句:
隐藏   复制代码
import { SearchComponent } from './Shared/search.component';
  1. 在声明部分添加SearchComponent以在任何组件中使用它:
隐藏   复制代码
declarations: [AppComponent, UserComponent, HomeComponent, UserFilterPipe, SearchComponent],
  1. 所以现在我们SearchComponent 已经准备好使用了,我们在这里使用它UserComponent。双击app -> Components -> user.component.html进行编辑:
  1. 我们将添加SearchComponent 在用户列表的顶部,因此在Add 按钮顶部添加以下div :
隐藏   复制代码
<div>
<search-list [title]='searchTitle' (change)="criteriaChange($event)"></search-list>
</div>
  1. 让我们明白一下,它看起来像普通的HTML,但是search-list标签。如果你记得,这是我们在文件中定义的selector 属性。如果您在第1部分中记住,我们了解到,用于将数据从父组件发送到子组件。我们通过我们定义的变量来评价孩子的组件变量的价值。二是事件绑定,我们创建的事件,我们提供的功能中,当发生变化的事件将执行每一次。将保存事件发送的任何值,在我们的例子中,我们发送每个字符用户将进入搜索文本框(参见功能)。SearchComponent search.component.tsProperty Binding [ ]titlesearchTitle UserComponent( )change SearchComponent criteriaChangeUserComponent $eventchange getEachChar SearchComponent
  2. 由于我们criteriaChange 在事件绑定中指定了函数,search-list所以我们将其添加到UserComponent。双击app -> Components -> user.component.ts进行编辑:
  1. 添加以下功能 user.component.ts
隐藏   复制代码
criteriaChange(value: string): void {
if (value != '[object Event]')
this.listFilter = value;
}
  1. 您可以看到我们从change 事件中获取输入参数值(用户在搜索文本框中输入的文本),并将其分配给listFilter 我们将用于pipe 过滤器的变量。让我们继续声明listFilter 变量。添加以下行与其他变量声明语句:
隐藏   复制代码
listFilter: string;
  1. 到目前为止,我们已经创建了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 的输入参数。
  2. 由于我们使用定义的双向数据绑定[(ngModel)]listFilter 变量FormsModule,我们将其添加到其中AppModule,以便更新
隐藏   复制代码
import { ReactiveFormsModule } from '@angular/forms';

隐藏   复制代码
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; in AppModule.
  1. 添加formsModule 在导入部分。
  1. 在任何浏览器中编译和运行项目(建议使用Firefox或Chrome)。转到UserManagement 页面,现在可能有很少的记录,你可以继续添加15,20多个。开始输入First Name Last NameGender 在搜索文本框中,您将看到运行时过滤的记录:
  1. 这就是我们的过滤。
  2. 接下来,我们将了解Angular2中的错误处理,我将保持简单,不会在每个错误类型中出现,但是让您知道如何对每个错误类型都有自定义错误。有关Angular2 ErrorHandler 类的快速参考,请点击此处
  3. 对于自定义错误处理程序类,我们可以扩展ErrorHandler has constructor handleError 使用error 参数的方法。误差参数具有完整的错误的信息,例如statuserror codeerror text等等取决于错误类型(HTTP,应用等)。这真的有助于自定义错误消息。ErrorHandler 处理任何类型的错误,例如未声明的变量/函数,任何数据异常或HTTP错误。此外ErrorHandler 我还将解释如何在Firefox浏览器中调试代码(您也可以在Chrome或IE中进行调试)。
  4. 我们将从中删除错误处理代码UserService UserComponent 以便我们可以捕获ErrorHandler 类中的所有错误,然后我们使用Firefox检查错误debugger。所以,让我们开始吧。
  5. 首先,我们创建自定义错误处理程序类。右键单击app -> Shared文件夹,然后选择Add -> TypeScript File
  1. 输入名称errorhandler.ts并点击OK 按钮:
  1. 将以下代码粘贴到新创建的文件中:
隐藏   复制代码
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);
}
}
  1. 代码是非常不言自明的评论,即为什么我们正在调用super(true)构造函数。AppErrorHandler 是我们的定制类,它正在扩展Angular2 ErrorHandler 类并实现该handleError 功能。在handleError, 我把调试器显示出来会有什么错误,以及如何自定义它。我们通过简单的JavaScript警报功能显示错误消息。
  2. 首先让我们看看HTTP 错误。假设我们在加载数据库中的所有用户之前都有认证逻辑,并且请求以某种方式未被认证,我们not authorized (401)将从ASP.NET Web API向Angular2 发送错误。让我们得到这个错误AppErrorHandler并检查它。
  3. 接下来,添加AppErrorHandlerin AppModule来捕获所有错误。添加以下import语句:

AppErrorHandler从... 导入'./Shared/errorhandler';

  1. 更新提供程序部分以具有ErrorHandler
隐藏   复制代码
providers: [{ provide: ErrorHandler, useClass: AppErrorHandler },{ provide: APP_BASE_HREF, useValue: '/' }, UserService]
  1. 我们告诉我们的模块使用我们的自定义错误处理程序来处理任何错误。不要忘记在ErrorHandler语句中添加类@angular2/core import引用:
  1. 我们来评论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
);
}
  1. 您可以看到我已经注释掉了在msg屏幕底部显示的变量中保存的错误语句。
  2. 接下来我们来评论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);
}
  1. 现在我们的客户端代码已经准备好捕获HTTP异常,我们来添加unauthorized 异常代码UserAPIController(基本上我们将添加它BaseAPIController并调用它UserAPIController)。
  2. 转到Controllers文件夹并双击BaseAPIController.cs进行编辑:
  1. 添加以下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;
}
  1. 由于我迄今没有任何身份验证逻辑,所以在UserAPIController 我只是更新Get()方法如下,只是替换了这个ToJson功能ErrorJson,现在API将永远抛出Unauthorized我们将尝试加载用户的excleion:
隐藏   复制代码
public HttpResponseMessage Get()
{
return ErrorJson(UserDB.TblUsers.AsEnumerable());
}
  1. 编译并运行项目,转到User Management页面。几分钟后,您会看到如下所示的丑恶警报信息:
  1. 很好,所以我们的测试环境成功创建。我们将这个错误从UserAPI Get()方法发送到我们的定制中被捕获的客户端AppErrorHandler
  2. 我们来调试错误,在errorhandler.ts文件中,点击旁边的灰色条debugger来设置断点:
  1. 在Firefox中运行应用程序,按Ctrl+Shift+S或单击打开menu button => Developer => Debugger
  1. 你应该结束以下屏幕:
  1. 转到User Management页面,稍后,您将看到执行停止在debugger
  1. 鼠标悬停在error上方,您会看到所有参数错误:
  1. 由于这是HTTP错误,你可以看到的HTTPStatusCode401 (Unauthorized request),主体部分仍然有一定你永远也不会发回的数据,而不是你可以在这里发送用户友好的错误消息。
  2. 通过考虑这些错误参数,我们可以通过检查状态代码来扩展我们的错误处理。我们开始做吧。
  3. 更新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);
}
  1. Compile run 应用程序再次,转到User Management页一次。您现在将看到以下用户友好的错误消息:
  1. Firefox debugger是调试客户端代码的好工具,花一些时间来探索更多有用的功能。你可以步骤next lineinto the function step out通过突出显示的按钮:
  1. 接下来我们来弄清我们的应用程序,并通过Firefox调试检查错误变量。双击 app => Components => home.component.ts进行编辑。
  2. 在模板部分输入以下html:
隐藏   复制代码
<button class="btn btn-primary" (click)="IdontExist()">ErrorButton</button>
  1. 最终模板应如下:
  1. 我添加了一个带有IdontExist()不存在的调用函数的click事件的按钮HomeComponent
  2. 让我们运行应用程序,然后运行调试器,你会ErrorButton在屏幕中间看到愚蠢:
  1. 点击ErrorButton,再次看到执行停止在debugger(断点),鼠标悬停错误,浏览参数弹出或点击watch链接在底部放错误变量在右侧Variables部分:
  1. 您可以看到这一堆新的信息,展开该originalError部分,您将看到实际的错误:
  1. 您可以看到非常详细的信息来挖掘复杂的错误。
  2. Resume左侧的按钮继续执行:
  1. 您会看到简短的错误信息:
  1. 调试是在客户端获取完整信息的好工具。
上一篇:并发研究之CPU缓存一致性协议(MESI)


下一篇:MVC和Web API 过滤器Filter [转]