-1

Sử dụng Angular Material Paginator với ASP.NET Core và Angular

Giới thiệu

Trong bài viết này tôi muốn giới thiệu tới các bạn làm thế nào để phân trang sử dụng table của Angular Material với Angular cùng với ASP.NET Core WebAPI. Với Angular Material Table và Paginator Module nó là khá dễ để cài đặt phân trang trong một cách hay, như vậy bạn có thể sử dụng nó trên phía client và chỉ hiển thị một số các bản ghi được chỉ định tới người dùng. Cái gì chúng ta không muốn làm ? Đó là việc lấy tất các items từ backend trong một lần đầu tiên để phân trang và tiếp theo hiển thị chỉ một số lượng nhất định. Thay vì thế chúng ta muốn chỉ lấy cái gì chúng ta cần và hiển thị cái đó. Nếu người dùng lcik vào button "next page" thì những items tiếp theo sẽ được hiển thị.

1. Backend

Backemd là một ASP.NET Core WebAPI cái mà response data kiểu dữ liệu JSON. Với data này, mỗi item đều chứa những links được chỉ định và cũng chứa tất cả các link dùng cho phân trang như: next, previous, ...Mặc dù vậy, chúng ta không cần chúng trong ví dụ này bởi vì chúng ta đã có một số implement logic từ Angular Material. Nếu bạn không muốn sử dụng Angular Material hoặc một "intelligent" UI khác mà cung cấp sẵn cho bạn paging logic, bạn có thể sử dụng links để làm tất cả bởi chính bạn.

Customer Controller

[Route("api/[controller]")]
public class CustomersController : Controller
{
	[HttpGet(Name = nameof(GetAll))]
	public IActionResult GetAll([FromQuery] QueryParameters queryParameters)
	{
		List<Customer> allCustomers = _customerRepository
			.GetAll(queryParameters)
			.ToList();

		var allItemCount = _customerRepository.Count();

		var paginationMetadata = new
		{
			totalCount = allItemCount,
			pageSize = queryParameters.PageCount,
			currentPage = queryParameters.Page,
			totalPages = queryParameters.GetTotalPages(allItemCount)
		};

		Response.Headers
			.Add("X-Pagination", 
				JsonConvert.SerializeObject(paginationMetadata));

		var links = CreateLinksForCollection(queryParameters, allItemCount);

		var toReturn = allCustomers.Select(x => ExpandSingleItem(x));

		return Ok(new
		{
			value = toReturn,
			links = links
		});
	}
}

Chúng ta đang gửi trở lại thông tin về phân trang với HATEOAS nhưng cũng với một header để Angular sẽ đọc nó sau. totalcount là một thông tin đặc việt thú vị với client. Bạn cũng có thể gửi trở lại với response JSON.

var paginationMetadata = new
{
    totalCount = allItemCount,
    // ...

};

Response.Headers
    .Add("X-Pagination", 
        JsonConvert.SerializeObject(paginationMetadata));

Nếu bạn gửi nó trở lại thông qua header, đảm bảo để mở rộng header trong CORS cái mà có thể đọc được trên phía client. Bạn có thể cấu hình điều này trong Startup.cs

services.AddCors(options =>
{
    options.AddPolicy("AllowAllOrigins",
        builder => builder.AllowAnyOrigin()
        .AllowAnyMethod()
        .AllowAnyHeader()
        .AllowCredentials()
        .WithExposedHeaders("X-Pagination"));
});

Có một tham số cái mà có thể được truyền tới phương thức GetAll: QueryParameters

public class QueryParameters
{
    private const int maxPageCount = 50;
    public int Page { get; set; } = 1;

    private int _pageCount = maxPageCount;
    public int PageCount
    {
        get { return _pageCount; }
        set { _pageCount = (value > maxPageCount) ? maxPageCount : value; }
    }
    
    public string Query { get; set; }

    public string OrderBy { get; set; } = "Name";
}

Modelbinder từ ASP.NET Core có thể ánh xạ các tham số trong request tới object trên và bạn có thể bắt đầu sử dụng chúng như link: http://localhost:5000/api/customers?pagecount=10&page=1&orderby=Name là một request hợp lệ sau đó đưa chúng ta khả năng bắt lấy chỉ những mảng items chúng ta muốn.

2. Frontend

Frontend được xây dựng với Angular và Angular Material. Xem chi tiết bên dưới.

PaginationService

Service này được sử dụng để sưu tập tất cả các thông tin về pagination. Chúng ta đang inject PaginationService và dùng giá trị của nó để tạo URL và gửi request.

@Injectable()
export class PaginationService {
    private paginationModel: PaginationModel;

    get page(): number {
        return this.paginationModel.pageIndex;
    }

    get selectItemsPerPage(): number[] {
        return this.paginationModel.selectItemsPerPage;
    }

    get pageCount(): number {
        return this.paginationModel.pageSize;
    }

    constructor() {
        this.paginationModel = new PaginationModel();
    }

    change(pageEvent: PageEvent) {
        this.paginationModel.pageIndex = pageEvent.pageIndex + 1;
        this.paginationModel.pageSize = pageEvent.pageSize;
        this.paginationModel.allItemsLength = pageEvent.length;
    }
}

Chúng ta đang show ra ba thuộc tính ở đây cái mà có thể thay đổi thông qua phương thức "change()". Phương thức có tham số pageEvent đến từ Angualr Material Paginator. Ở đó mọi thông tin về trạng thái paging hiện tại đều được lưu trữ. Chúng ta đang truyền điều này xung quanh để có được thông tin về trạng thái paging

HttpBaseService

@Injectable()
export class HttpBaseService {

    private headers = new HttpHeaders();
    private endpoint = `http://localhost:5000/api/customers/`;

    constructor(
        private httpClient: HttpClient,
        private paginationService: PaginationService) {

        this.headers = this.headers.set('Content-Type', 'application/json');
        this.headers = this.headers.set('Accept', 'application/json');
    }

    getAll<T>() {
        const mergedUrl = `${this.endpoint}` +
            `?page=${this.paginationService.page}&pageCount=${this.paginationService.pageCount}`;

        return this.httpClient.get<T>(mergedUrl, { observe: 'response' });
    }

    getSingle<T>(id: number) {
        return this.httpClient.get<T>(`${this.endpoint}${id}`);
    }

    add<T>(toAdd: T) {
        return this.httpClient.post<T>(this.endpoint, toAdd, { headers: this.headers });
    }

    update<T>(url: string, toUpdate: T) {
        return this.httpClient.put<T>(url,
            toUpdate,
            { headers: this.headers });
    }

    delete(url: string) {
        return this.httpClient.delete(url);
    }
}

Chúng ta đang inject PagingService và dùng giá trị của nó để tạo url gửi tới request.

Components

Bên cạnh những services, các components dùng những services này và các giá trị. Chúng đang tương tác với event pageswitch và được chia ra các components có trạng thái và không trạng thái

Include in module

Trong ListComponents bây giờ chúng ta đang sử dụng paginator module, nhưng đầu tiên chúng phải include nó trong app module giống như bên dưới

import { MatPaginatorModule } from '@angular/material/paginator';

@NgModule({
    imports: [
        MatPaginatorModule,
        // ...

    ]
})

Và sử dụng nó trong module như code sau:

ListComponent

<div class="example-container mat-elevation-z8">
    <mat-table #table [dataSource]="dataSource" matSort>
  
      <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.id}} </mat-cell>
      </ng-container>
  
      <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.name}} </mat-cell>
      </ng-container>
  
      <ng-container matColumnDef="created">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Created </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{element.created | date}} </mat-cell>
      </ng-container>
  
      <ng-container matColumnDef="actions">
        <mat-header-cell *matHeaderCellDef mat-sort-header> Actions </mat-header-cell>
        <mat-cell *matCellDef="let element"> 
          <button mat-icon-button (click)="onDeleteCustomer.emit(element)"><mat-icon>delete</mat-icon></button> 
          <a mat-icon-button [routerLink]="['/details', element.id]"><mat-icon>edit</mat-icon></a>
        </mat-cell>
      </ng-container>
  
      <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
    </mat-table>
  </div>
  
  
  <mat-paginator [length]="totalCount"
    [pageSize]="paginationService.pageSize"
    [pageSizeOptions]="paginationService.selectItemsPerPage" 
    (page)="onPageSwitch.emit($event)">
  </mat-paginator>

pageSizepageSizeOptions đến từ PaginationService cái mà inject ngầm định trong component.

export class ListComponent {

    dataSource = new MatTableDataSource<Customer>();
    displayedColumns = ['id', 'name', 'created', 'actions'];

    @Input('dataSource')
    set allowDay(value: Customer[]) {
        this.dataSource = new MatTableDataSource<Customer>(value);
    }

    @Input() totalCount: number;
    @Output() onDeleteCustomer = new EventEmitter();
    @Output() onPageSwitch = new EventEmitter();

    constructor(public paginationService: PaginationService) { }
}

OverviewComponent

<app-list 
    [dataSource]="dataSource" 
    [totalCount]="totalCount"
    (onDeleteCustomer)="delete($event)"
    (onPageSwitch)="switchPage($event)"
</app-list>
export class OverviewComponent implements OnInit {

    dataSource: Customer[];
    totalCount: number;

    constructor(
        private customerDataService: CustomerDataService,
        private paginationService: PaginationService) { }

    ngOnInit(): void {
        this.getAllCustomers();
    }

    switchPage(event: PageEvent) {
        this.paginationService.change(event);
        this.getAllCustomers();
    }

    delete(customer: Customer) {
        this.customerDataService.fireRequest(customer, 'DELETE')
            .subscribe(() => {
                this.dataSource = this.dataSource.filter(x => x.id !== customer.id);
            });
    }

    getAllCustomers() {
        this.customerDataService.getAll<Customer[]>()
            .subscribe((result: any) => {
                this.totalCount = JSON.parse(result.headers.get('X-Pagination')).totalCount;
                this.dataSource = result.body.value;
            });
    }
}

Phương thức switchPage được gọi khi page thay đổi và đầu tiền thiết lập tất cả các giá trị mới trong paginationService và sau đó nhận customers trở lại. Những giá trị này sau đó được cung cấp lại trong dataService và được dùng ở đó, và cũng được sử dụng trong view nơi mà chúng được hiển thị một cách chính xác. Trong phương thức getAllCustomers chúng ta đang đọc giá trị totalCount từ header. Đảm bảo để đọc đầy đủ response trong dataService bởi việc thêm return his.httpClient.get<T>(mergedUrl, { observe: 'response' }); và show ra header trong các tùy chọn CORS giống như đã được show trước đó trong bài viết này.

Bài viết nguồn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí