65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (27投票s)

2017年5月14日

CPOL

17分钟阅读

viewsIcon

62816

downloadIcon

2456

在本部分中,我们将增强用户管理应用程序,以具备搜索/过滤、全局错误处理和调试功能。

系列文章

引言

ASP.NET MVC & Web API - 第 1 部分 中,我们学习了 ASP.NET MVC 中 Angular2 的基本设置。在本部分中,我们将学习

  • 如何使用 Angular2 的 pipeUserComponent 中实现搜索/过滤功能,通过 FirstNameLastNameGender 搜索用户?
  • 如何通过扩展 ErrorHandler 类来实现全局错误处理?
  • 如何使用 Firefox 调试器调试客户端代码?

开始吧

  1. 首先,请查看 ASP.NET MVC & Web API - 第 1 部分 并下载附带的 Angular2MVC_Finish.zip 文件。将其解压到您计算机上的任意位置,然后双击 Angular2MVC.sln 在 Visual Studio (2015 Update 3 或 2017) 中打开解决方案。

  2. 由于 Angular2MVC 解决方案没有所需的 NuGetnode_modules 包,请转到 **Build** 菜单,然后选择 **Rebuild Solution**。Visual Studio 将下载 packages.config 中列出的所有 .NET 包以及 package.json 中提到的客户端包。

  3. 您会发现 packages 文件夹包含所有必需的 .NET 包,以及 node_modules 文件夹包含所有客户端包。

  4. 编译并运行应用程序,您应该不会收到任何错误。
  5. 既然现在已经下载了所有项目依赖项,让我们开始实现搜索/过滤功能,以响应用户输入,过滤实现后的最终页面将如下所示:

  6. 从截图来看,您可能已经对搜索/过滤功能的工作方式有了一些了解。一旦用户在 **Search** 文本框中开始输入文本,数据就会在列表中实时过滤,匹配用户输入的文本与 **First Name**、**last Name** 和 **Gender** 字段。这因为是客户端过滤,所以非常方便快捷。我喜欢这个功能,因为它避免了难看的带搜索按钮的文本框和复杂的服务器端过滤查询。
  7. 因此,在接下来的步骤中,我们将学习如何实现此功能。我们将使用 Angular2 的 pipe 来过滤数据,但在开始编写代码之前,让我们先了解一下 pipe 是什么以及如何使用它。
  8. 虽然 Angular2 文档对此有非常简单全面的解释 在此,但对于我这种懒人来说,让我为您总结一下。pipe 将数据转换为有意义的表示形式,例如,您从数据库获取日期 12/03/2016,并希望将其转换为 Dec 03, 2016。您可以通过 pipe 来实现。其他内置 Pipes 包括 Uppercaselowercasejson 等,这些名称一看就明白。为什么叫 pipe,我认为是因为我们使用 | 符号将其应用于变量或值。例如:{{Value | uppercase}}。您可以根据需要将任意数量的 pipes 应用于特定值,用 | 分隔,例如:{{ birthdate | date | uppercase }}。您也可以通过 : (冒号)pipe 指定参数,例如,日期过滤器可以接受格式参数:{{birthdate | date : ‘MM/dd/yyyy’}}
  9. 现在我们对 pipe 有了基本了解,让我们通过 pipe 实现用户搜索功能。就像 Angular2 中可用的内置 pipes 一样,我们也可以实现自己的自定义 pipes。我们只需要实现 PipeTransform 接口,并在 transform 方法中开发自定义逻辑,该方法接受两个参数:data(要过滤的数据源)和 optional arguments(例如,要在数据中搜索的用户输入 string)。要了解更多关于自定义 pipes 的信息,请单击 此处
  10. 让我们创建一个 user 过滤 pipe。右键单击 app 文件夹,然后选择 Add -> New Folder,将文件夹命名为 filter(或 pipe,随您喜欢)。

  11. 右键单击新创建的 filter 文件夹,然后选择 **Add -> TypeScript File**

  12. 在 **Item name** 文本框中输入 user.pipe.ts,然后单击 **OK** 按钮。

  13. 将以下代码粘贴到新添加的 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;
    
      }
    }
  14. 让我们来理解一下我们刚刚在 user.pipe.ts 中添加的内容。
    1. 第一行,我们导入了 PipeTransformPipe 接口,我们将实现它们来实现过滤功能。
    2. 第二行,我们导入了第一部分中创建的 IUser 接口,用于保存用户列表。在这里,我们也使用它来保存用户列表,这是过滤的源数据。
    3. 下一行,我们通过 userFilter 指定了 pipe 的 selector/名称,我们将使用该名称来使用 pipe(您将在后续步骤中找到具体方法)。
    4. 接下来,我们创建了 UserFilterPipe 类,该类实现了 PipeTransform 接口(实现接口意味着为接口中提到的所有方法提供主体)。
    5. 右键单击 PipeTransform,然后选择 **Go To Definition** 选项。

    6. 您将被带到 pipe_transform_d.ts 文件,在那里您将找到关于如何使用 pipe 的简洁描述和示例,以及我们必须实现的 transform 方法。

    7. 所以,让我们回到 user.pipe.ts。可以看到我们有一个 transform 方法,第一个参数是 IUser 数组,第二个参数名为 filter,它是要搜索的输入 string,用于在 IUser 数组中进行搜索。
    8. transform 方法中,第一行只是检查 filter 是否不为 null
    9. 下一条语句是实际的搜索实现。如果您是 C# 开发人员,可以将其与 LINQ to Object 进行比较。我们调用 Array 的 filter 方法,通过 条件运算符 检查 IUser 的任何成员(FirstNameLastNameGender)是否与用户输入的搜索 string 匹配,如果匹配,则返回过滤后的结果。toLocaleLowerCase 方法将 string 转换为小写。要了解更多信息,请单击 此处。如果用户列表中没有匹配的记录,我们将返回所有行。
  15. 现在我们的过滤器已准备就绪,让我们将其添加到 AppModule 以便在应用程序中使用它。双击 app 文件夹中的 app.module.ts 文件进行编辑。

  16. 根据以下屏幕截图更新 AppModule

  17. 我们通过 import 语句和声明部分添加了 UserFilterPipe 的引用。仅供回顾,declaration 部分中的组件可以相互识别,这意味着我们可以在 UserComponent(或任何其他组件)中使用 UserFilterPipe,而无需在 UserComponent 本身中添加引用。我们可以在 declaration 部分声明 componentspipes 等。
  18. 因此,我们的用户过滤/搜索功能已准备就绪。下一步是在 UserComponent 中使用它。但我们不直接在 UserComponent 中使用它,而是创建一个共享的 SearchComponent,所有组件都可以共享。这将帮助我们理解
    1. 父级UserComponent)和 子级SearchComponent)组件之间的交互。
    2. 如何通过 @Input 发送输入参数,并通过 @Output 别名获取值。
  19. 右键单击主 app 文件夹中的 Shared 文件夹,然后选择 **Add -> TypeScript File**

  20. 将 **Item name** 输入为 search.component.ts,然后单击 **OK** 按钮。

  21. 将以下代码复制到 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 类。InputOutput 接口不言自明,用于从 UserComponent 接收输入参数(在本例中为来自用户的搜索字符串)。Output 用于从 SearchComponent 将值发送回,但这有点意思,输出是通过使用 EventEmitter 类的事件发送回的。在后续步骤中,这一点将更加清晰。
    2. 下一行,我们提供了 Component 元数据,即 selector(我们将使用 SearchComponent 的标签名称在 UserComponent 中,例如 <search-list></search-list>)。template 是组件的 HTML 部分。您也可以将其放在单独的 HTML 文件中,并指定 templateUrl 属性,但由于它非常精简,我倾向于将其放在同一个文件中。
    3. SearchComponent 类中,我们声明了一个局部变量 listFilter,它是我们将在 <div class="h3 text-muted">Filter by: {{listFilter}}</div> 中显示的搜索 string。这仅用于化妆目的,显示我们正在搜索的内容。
    4. 第二个变量 title 带有 @Input 装饰器。我们将从 UserComponent 发送搜索 textbox 的标题。第三个变量 change 带有 @Output 装饰器,类型为 EventEmitter。这是我们将数据发送回父组件的方式。change EventEmitter<string> 表示 change 是父组件需要订阅的事件,它将接收 string 参数类型。我们将显式调用 emit 函数(即 change.emit(“test”))将值发送回父组件。
    5. getEachChar(value: any):每当用户在搜索文本框中输入字符时,都会调用此函数。我们只调用 this.change.emit(value);,它会将该字符发送到父组件,然后发送到 UserFilterPipe pipe,以从 User 列表中过滤。仅供回顾,在 UserPipeFilter 中,我们将该字符与 FirstNameLastNameGender 进行比较,并仅返回包含该字符(s) 的记录。因此,只要用户在搜索文本框中输入字符,数据就会实时过滤。
    6. clearFilter():将清除过滤器,将 User 列表重置为默认状态,不进行任何过滤。
    7. getPasteData(value: any):这是一个稍微有趣的功能,它将处理用户从其他地方复制搜索字符串并将其粘贴到搜索文本框中以过滤 Users 列表的情况。通过 value.clipboardData.getData('text/plain'),我们获取粘贴的数据,并通过 change.emit(value) 函数将其发送到父组件。
    8. 现在我们对这些函数有了一些了解。如果您回到 SearchComponent template(HTML),我们在 keyup 事件上调用 getEachChar,当用户在搜索文本框中键入时,它将触发。getPasteDatapaste 事件上被调用,当用户在搜索文本框中粘贴值时会发生。clearFilter 函数将在单击交叉图像时调用,该图像仅在搜索文本框中至少有一个字符时可见。
  22. 因此,我们完成了 SearchComponent 的创建,并希望您了解其工作原理。让我们将其添加到 AppModule 中,以便我们可以使用它。双击 app -> app.module.ts 进行编辑。

  23. 添加以下 import 语句。
    import { SearchComponent } from './Shared/search.component';
  24. 在 declarations 部分添加 SearchComponent 以便在任何组件中使用它。
    declarations: [AppComponent, UserComponent, 
                   HomeComponent, UserFilterPipe, SearchComponent],

  25. 现在我们的 SearchComponent 已准备好使用。让我们在 UserComponent 中使用它。双击 **app -> Components ->** user.component.html 进行编辑。

  26. 我们将 SearchComponent 添加到 User 列表的顶部。因此,在 **Add** 按钮的顶部附加以下 div
    <div>
       <search-list [title]='searchTitle' (change)="criteriaChange($event)"></search-list>
    </div>
  27. 让我们来理解一下。它看起来像普通的 HTML,但带有 search-list 标签。如果您还记得,这就是我们在 search.component.ts 文件中为 SearchComponent 定义的 selector 属性。如果您还记得在第 1 部分中,我们学习了 Property Binding [ ],它用于将数据从父组件发送到子组件。我们通过 UserComponent 中定义的 searchTitle 变量将值赋给子组件的 title 变量。第二个是事件绑定 ( ),我们在 SearchComponent 中创建了 change 事件,并且我们在 UserComponent 中提供了 criteriaChange 函数,当 change 事件发生时,该函数将执行。$event 将包含 change 事件发送的任何值。在本例中,我们发送用户在搜索文本框中输入的每个字符(请参阅 SearchComponent 中的 getEachChar 函数)。这就是我们如何从子组件获取值。
  28. 由于我们在 search-list 的事件绑定中指定了 criteriaChange 函数,所以让我们将其添加到 UserComponent 中。双击 **app -> Components ->** user.component.ts 进行编辑。

  29. user.component.ts 中添加以下函数。
    criteriaChange(value: string): void {
        if (value != '[object Event]') 
        this.listFilter = value;
    }
  30. 您可以看到我们正在获取输入参数值(用户在搜索文本框中输入的文本)来自 change 事件,并将其分配给我们将用于 pipe 过滤的 listFilter 变量。让我们继续声明 listFilter 变量。将以下行与其他变量声明语句一起添加。
    listFilter: string;
  31. 到目前为止,我们已经创建了 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 是过滤的输入参数。
  32. 由于我们为 listFilter 变量使用了 [(ngModel)] 进行双向数据绑定,而这在 FormsModule 中定义,所以让我们将其添加到 AppModule 中。更新
    import { ReactiveFormsModule } from '@angular/forms';

    to

    import { FormsModule, ReactiveFormsModule } from '@angular/forms'; in AppModule.
  33. 在 imports 部分添加 formsModule

  34. 在任何浏览器(推荐 Firefox 或 Chrome)中编译并运行项目。转到 UserManagement 页面。目前可能只有少量记录,您可以添加 15、20 条更多记录。开始在 **Search** 文本框中输入 First NameLast NameGender,您将看到记录在运行时被过滤。

  35. 这就是我们的过滤功能。
  36. 接下来,我们将学习 Angular2 中的错误处理。我将保持简单,不深入探讨每种错误类型,但会告诉您如何为每种错误类型自定义错误。有关 Angular2 ErrorHandler 类的快速参考,请单击 此处
  37. 对于自定义错误处理程序类,我们可以扩展 ErrorHandler 类,该类具有 constructorhandleError 方法,并带有 error 参数。error 参数包含完整的错误信息,例如 statuserror codeerror text 等,具体取决于错误类型(HTTP、应用程序等)。这对于自定义错误消息非常有帮助。ErrorHandler 可以处理任何类型的错误,例如未声明的变量/函数、任何数据异常或 HTTP 错误。除了 ErrorHandler,我还会解释如何使用 Firefox 浏览器调试代码(您也可以在 Chrome 或 Internet Explorer 中进行调试)。
  38. 我们将注释掉 UserServiceUserComponent 中的错误处理代码,以便我们可以捕获 ErrorHandler 类中的所有错误,然后使用 Firefox debugger 检查错误。那么,让我们开始吧。
  39. 首先,让我们创建自定义错误处理程序类。右键单击 **app ->** Shared 文件夹,然后选择 **Add -> TypeScript File**

  40. 输入 errorhandler.ts 作为名称,然后单击 **OK** 按钮。

  41. 将以下代码粘贴到新创建的文件中。
    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);
       }
    }
  42. 代码本身通过注释很容易理解,即为什么我们在构造函数中调用 super(true)AppErrorHandler 是我们的自定义类,它扩展了 Angular2 的 ErrorHandler 类并实现了 handleError 函数。在 handleError 中,我放了一个 debugger 来向您展示将出现什么错误以及如何自定义它。我们通过简单的 JavaScript alert 函数显示错误消息。
  43. 首先,让我们看看 HTTP 错误。假设我们在从数据库加载所有用户之前有身份验证逻辑,并且请求由于某种原因未经验证,我们将从 ASP.NET Web API 发送 not authorized (401) 错误到 Angular2。让我们在 AppErrorHandler 中获取此错误并进行检查。
  44. 接下来,将 AppErrorHandler 添加到 AppModule 以捕获所有错误。添加以下 import 语句。
    import AppErrorHandler from './Shared/errorhandler';
  45. 更新 provider 部分以包含 ErrorHandler
    providers: [{ provide: ErrorHandler, useClass: AppErrorHandler },
                { provide: APP_BASE_HREF, useValue: '/' }, UserService]
  46. 我们告诉我们的模块使用自定义错误处理程序来处理任何类型的错误。不要忘记在 @angular2/core import 语句中添加 ErrorHandler 类的引用。

  47. 让我们注释掉 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
                            );
                      }
  48. 您可以看到我注释掉了保存到 msg 变量以显示在屏幕底部的错误语句。
  49. 接下来,让我们注释掉 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);
    }
  50. 现在我们的客户端代码已准备好捕获 HTTP 异常。让我们在 UserAPIController 中添加 unauthorized 异常代码(基本上,我们将其添加到 BaseAPIController 中,然后在 UserAPIController 中调用它)。
  51. 转到 Controllers 文件夹,然后双击 BaseAPIController.cs 进行编辑。

  52. 添加以下 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;
    }
  53. 由于我目前没有任何身份验证逻辑,所以在 UserAPIController 中,我仅按如下方式更新 Get() 方法,只是将 ToJson 函数替换为 ErrorJson。现在当我们尝试加载用户时,API 始终会抛出 Unauthorized 异常。
    public HttpResponseMessage Get()
       {
          return ErrorJson(UserDB.TblUsers.AsEnumerable());
      }
  54. 编译并运行项目,转到 **User Management** 页面。片刻之后,您将看到一个如下所示的丑陋的警报消息。

  55. 太好了,我们的测试环境已成功创建。我们已将此错误从 UserAPI Get() 方法发送到客户端,并在我们的自定义 AppErrorHandler 中捕获了它。
  56. 让我们调试错误。在 errorhandler.ts 文件中,单击 debugger 旁边的灰色条以设置断点。

  57. 在 Firefox 中运行应用程序,按 **Ctrl+Shift+S** 或单击打开 **menu button => Developer => Debugger**。

  58. 您应该会看到以下屏幕。

  59. 转到 **User Management** 页面。片刻之后,您会发现执行在 debugger 处停止。

  60. 将鼠标悬停在 error 上,您将看到错误中的所有参数。

  61. 由于这是一个 HTTP 错误,您可以看到 HTTPStatusCode401 (Unauthorized request)。body 部分仍然包含数据,您肯定永远不会将其发送回,而是可以在此处发送用户友好的错误消息。
  62. 通过考虑这些错误参数,我们可以通过检查状态代码来扩展我们的错误处理。让我们这样做。
  63. 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);
    }
  64. **编译** 并**运行** 应用程序。再次转到 User Management 页面。您现在将看到以下用户友好的错误消息。

  65. Firefox debugger 是一个很棒的客户端代码调试工具,花一些时间探索更多有用的功能。您可以通过高亮显示的按钮 **step to next line**、**step into the function** 或 **step out**。

  66. 接下来,让我们弄乱我们的应用程序,看看通过 Firefox 调试器在 error 变量中会有什么。双击 **app => Components =>** home.component.ts 进行编辑。
  67. 在 template 部分输入以下 HTML。
    <button class="btn btn-primary" (click)="IdontExist()">ErrorButton</button>
  68. 最终模板应如下所示。

  69. 我添加了一个带有 click 事件的按钮,该按钮调用 IdontExist() 函数,该函数在 HomeComponent 中不存在。
  70. 让我们运行应用程序,然后运行调试器。您会在屏幕中间看到一个愚蠢的 ErrorButton

  71. 单击 ErrorButton,您会再次看到执行在 debugger(断点)处停止。将鼠标悬停在 error 上,浏览弹出窗口中的参数,或单击底部的 watch 链接,将 error 变量添加到右侧的 Variables 部分。

  72. 您可以看到这一大堆新信息。展开 originalError 部分,您将看到实际的错误。

  73. 您可以看到非常详细的信息,用于深入研究复杂的错误。
  74. 按左侧的 **Resume** 按钮继续执行。

  75. 您将看到简短的错误消息。

  76. 调试是获取客户端完整信息的绝佳工具。

历史

  • 2017年5月13日:创建
© . All rights reserved.