مطالب
بررسی روش آپلود فایل‌ها از طریق یک برنامه‌ی Angular به یک برنامه‌ی ASP.NET Core
پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


تدارک مقدمات مثال این قسمت

این مثال در ادامه‌ی همین سری کار با فرم‌های مبتنی بر قالب‌ها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه می‌کنیم:
 >ng g m UploadFile -m app.module --routing
همچنین به فایل app.module.ts مراجعه کرده و UploadFileModule را بجای UploadFileRoutingModule در قسمت imports معرفی می‌کنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت یک درخواست پشتیبانی را اضافه خواهیم کرد:
 >ng g c UploadFile/UploadFileSimple
که اینکار سبب به روز رسانی فایل upload-file.module.ts و افزوده شدن UploadFileSimpleComponent به قسمت declarations آن می‌شود.
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف می‌کنیم:
 >ng g cl UploadFile/Ticket
با این محتوا:
export class Ticket {
  constructor(public description: string = "") {}
}
در اینجا Ticket تعریف شده دارای یک خاصیت توضیحات است و این فرم به همراه فیلد ارسال چندین فایل نیز می‌باشد که نیازی به درج آن‌ها در کلاس فوق نیست:



ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن

پس از ایجاد ساختار کلاس Ticket، یک وهله از آن‌را به نام model ایجاد کرده و در اختیار قالب آن قرار می‌دهیم:
import { Ticket } from "./../ticket";

export class UploadFileSimpleComponent implements OnInit {
  model = new Ticket();
سپس قالب این کامپوننت و یا همان فایل upload-file-simple.component.html را به صورت ذیل تکمیل می‌کنیم:
<div class="container">
  <h3>Support Form</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group" [class.has-error]="description.invalid && description.touched">
      <label class="control-label">Description</label>
      <input #description="ngModel" required type="text" class="form-control"
        name="description" [(ngModel)]="model.description">
      <div *ngIf="description.invalid && description.touched">
        <div class="alert alert-danger"  *ngIf="description.errors.required">
          description is required.
        </div>
      </div>
    </div>

    <div class="form-group">
      <label class="control-label">Screenshot(s)</label>
      <input #screenshotInput required type="file" multiple (change)="fileChange($event)"
        class="form-control" name="screenshot">
    </div>

    <button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button>
  </form>
</div>
در اینجا ابتدا فیلد توضیحات درخواست جدید، ارائه و به خاصیت model.description متصل شده‌است. همچنین این فیلد با ویژگی required مزین، و اجباری بودن آن بررسی گردیده‌است.
سپس در انتها، فیلد آپلود را مشاهده می‌کنید؛ با این ویژگی‌ها:
الف) ngModel ایی به آن متصل نشده‌است؛ چون روش کار با آن متفاوت است.
ب) یک template reference variable به نام screenshotInput# در آن تعریف شده‌است. از این متغیر، در کامپوننت قالب استفاده خواهیم کرد.
ج) به رخ‌داد change این کنترل، متد fileChange متصل شده‌است که رخ‌داد جاری را نیز دریافت می‌کند.
د) ذکر ویژگی استاندارد multiple را نیز در اینجا مشاهده می‌کنید. وجود آن سبب خواهد شد تا کاربر بتواند چندین فایل را با هم انتخاب کند. اگر نیازی به ارسال چندین فایل نیست، این ویژگی را حذف کنید.


دسترسی به المان ارسال فایل در کامپوننت متناظر

تا اینجا یک المان ارسال فایل را به فرم، اضافه کرده‌ایم. اما چگونه باید به فایل‌های آن برای ارسال به سرور دسترسی پیدا کنیم؟
برای این منظور در ادامه دو روش را بررسی خواهیم کرد:

1) دسترسی به المان ارسال فایل از طریق رخ‌داد change
در تعریف فیلد ارسال فایل، اتصال به رخ‌داد change تعریف شده‌است:
 (change)="fileChange($event)"
معادل آن در سمت کامپوننت متناظر، به صورت ذیل است:
fileChange(event) {
    const filesList: FileList = event.target.files;
    console.log("fileChange() -> filesList", filesList);
}
همانطور که مشاهده می‌کنید، event.target، امکان دسترسی مستقیم به المان متناظری را در قالب کامپوننت میسر می‌کند. سپس می‌توان به خاصیت files آن دسترسی یافت.


در اینجا ساختار شیء استاندارد FileList و اجزای آن‌را مشاهده می‌کنید. برای مثال چون دو فایل انتخاب شده‌است، این لیست به همراه یک خاصیت طول و دو شیء File است.

تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آن‌ها را شناسایی و intellisense متناظری را مهیا می‌کند:
 C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
همچنین اگر به فایل tsconfig.json پروژه نیز مراجعه کنید، یک چنین تعاریفی در آن قرار دارند:
{
    "lib": [
      "es2016",
      "dom"
    ]
  }
}
وجود و تعریف کتابخانه‌ی dom است که سبب کامپایل شدن کدهای فوق، بدون بروز هیچگونه خطایی می‌شود.


2) دسترسی به المان آپلود فایل از طریق یک template reference variable
در حین تعریف المان فایل در فرم برنامه، متغیر screenshotInput# نیز ذکر شده‌است. می‌توان به یک چنین متغیرهایی در کامپوننت متناظر به روش ذیل دسترسی یافت:
import { Component, OnInit, ViewChild, ElementRef } from "@angular/core";

export class UploadFileSimpleComponent implements OnInit {
  @ViewChild("screenshotInput") screenshotInput: ElementRef;

  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);
  }
ابتدا یک خاصیت جدید را به نام screenshotInput از نوع ElementRef که در angular/core@ تعریف شده‌است، اضافه می‌کنیم. سپس برای اتصال آن به template reference variable ایی به نام screenshotInput، از ویژگی به نام ViewChild، با پارامتری مساوی نام همین متغیر، استفاده خواهیم کرد.
اکنون خاصیت screenshotInput کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شده‌است. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه می‌کنید، می‌توان به خاصیت files این کنترل ارسال فایل‌ها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شده‌است.


ارسال فرم درخواست پشتیبانی به سرور

تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون می‌خواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
 >ng g s UploadFile/UploadFileSimple -m upload-file.module
که سبب به روز رسانی خودکار قسمت providers فایل upload-file.module.ts نیز می‌شود.
در ادامه کدهای کامل این سرویس را مشاهده می‌کنید:
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Ticket } from "./ticket";

@Injectable()
export class UploadFileSimpleService {
  private baseUrl = "api/SimpleUpload";

  constructor(private http: Http) {}

  private extractData(res: Response) {
    const body = res.json();
    return body || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    if (!filesList || filesList.length === 0) {
      return Observable.throw("Please select a file.");
    }

    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }

    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }

    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
توضیحات تکمیلی:
روش کار با فرم‌هایی که فیلدهای ارسال فایل را به همراه دارند، متفاوت است با روش کار با فرم‌های معمولی. در فرم‌های معمولی، اصل شیء Ticket را به متد this.http.post واگذار می‌کنیم. مابقی آن خودکار است. در اینجا باید شیء استاندارد FormData را تشکیل داده و سپس اطلاعات را از طریق آن ارسال کنیم:
الف) افزودن مقادیر خواص شیء Ticket به FormData
  postTicket(ticket: Ticket, filesList: FileList): Observable<any> {
    const formData: FormData = new FormData();

    for (const key in ticket) {
      if (ticket.hasOwnProperty(key)) {
        formData.append(key, ticket[key]);
      }
    }
با استفاده از حلقه‌ی for می‌توان بر روی خواص یک شیء جاوا اسکریپتی حرکت کرد. به این ترتیب می‌توان نام و مقدار آن‌ها را یافت و سپس به formData به صورت key/value افزود.

ب) افزودن فایل‌ها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایل‌های فرم است:
    for (let i = 0; i < filesList.length; i++) {
      formData.append(filesList[i].name, filesList[i]);
    }
این مورد نیز به سادگی تشکیل یک حلقه، بر روی خاصیت files المان آپلود فایل است. به همین جهت بود که به دو روش سعی کردیم، به این خاصیت دسترسی پیدا کنیم.

یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمی‌توان در سمت سرور از روش model binding استفاده کرد. چون این نام دیگر ثابت نیست و هربار می‌تواند متغیر باشد (در حالت model binding دقیقا مشخص است که کلید مشخصی قرار است به سرور ارسال شود و بر همین اساس، نام خاصیت یا پارامتر سمت سرور تعیین می‌گردد). به همین جهت در سمت سرور برای دسترسی به این مجموعه، از روش Request.Form.Files استفاده می‌کنیم.

ج) ارسال اطلاعات نهایی به سرور
اکنون که formData را بر اساس اطلاعات اضافی ticket و فایل‌های متصل به آن تشکیل دادیم، روش ارسال آن به سرور همانند قبل است:
    const headers = new Headers();
    headers.append("Accept", "application/json");
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(`${this.baseUrl}/SaveTicket`, formData, options)
      .map(this.extractData)
      .catch(this.handleError);

یک نکته: در اینجا در روش استفاده از formData نباید Content-Type را به multipart/form-data  تنظیم کرد. در غیراینصورت خطای Missing content-type boundary error را دریافت می‌کنید.


تکمیل کامپوننت ارسال درخواست پشتیبانی

پس از تکمیل سرویس ارسال اطلاعات به سمت سرور، اکنون نوبت به استفاده‌ی از آن در کامپوننت ارسال فرم درخواست پشتیبانی است. بنابراین ابتدا این سرویس جدید را به سازنده‌ی UploadFileSimpleComponent تزریق می‌کنیم:
import { UploadFileSimpleService } from "./../upload-file-simple.service";

export class UploadFileSimpleComponent implements OnInit {
  constructor(private uploadService: UploadFileSimpleService  ) {}
و سپس متد submitForm چنین شکلی را پیدا می‌کند:
  submitForm(form: NgForm) {
    const fileInput: HTMLInputElement = this.screenshotInput.nativeElement;
    console.log("fileInput.files", fileInput.files);

    this.uploadService
      .postTicket(this.model, fileInput.files)
      .subscribe(data => {
        console.log("success: ", data);
      });
  }
در اینجا this.model حاوی اطلاعات شیء ticket است (برای مثال اطلاعات توضیحات آن) و fileInput.files امکان دسترسی به اطلاعات فایل‌های انتخابی توسط کاربر را می‌دهد. پس از آن فراخوانی متدهای this.uploadService.postTicket و subscribe، سبب ارسال این اطلاعات به سمت سرور می‌شوند.


دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیره‌ی فایل‌های آن‌

کدهای کامل SimpleUpload که در سرویس فوق مشخص شده‌است، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Ticket
    {
        public int Id { set; get; }
        public string Description { set; get; }
    }
}
و سپس کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class SimpleUploadController : Controller
    {
        private readonly IHostingEnvironment _environment;
        public SimpleUploadController(IHostingEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket(Ticket ticket)
        {
            //TODO: save the ticket ... get id
            ticket.Id = 1001;

            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            var files = Request.Form.Files;
            foreach (var file in files)
            {
                //TODO: do security checks ...!

                if (file == null || file.Length == 0)
                {
                    continue;
                }

                var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                using (var fileStream = new FileStream(filePath, FileMode.Create))
                {
                    await file.CopyToAsync(fileStream).ConfigureAwait(false);
                }
            }

            return Created("", ticket);
        }
    }
}
توضیحات تکمیلی
- تزریق IHostingEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به مسیر wwwroot سایت دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
 SaveTicket(Ticket ticket)
اما همانطور که عنوان شد، چون در حلقه‌ی افزودن فایل‌ها در سمت کلاینت، کلید نام این فایل‌ها هربار متفاوت است:
 formData.append(filesList[i].name, filesList[i]);
مجبور هستیم در سمت سرور بر روی Request.Form.Files یک حلقه را تشکیل داده و تمام فایل‌های رسیده را پردازش کنیم:
var files = Request.Form.Files;
foreach (var file in files)



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
در قسمت قبل، کار پیاده سازی سمت سرور نمایش اطلاعات یک گرید، به پایان رسید. در این قسمت می‌خواهیم از سمت کلاینت، اطلاعات صفحه بندی و مرتب سازی را به سمت سرور ارسال کرده و همچنین نتیجه‌ی دریافتی از سرور را نمایش دهیم.



پیشنیازهای نمایش اطلاعات گرید به همراه صفحه بندی اطلاعات

در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» نحوه‌ی نصب و معرفی کتابخانه‌ی ngx-bootstrap را بررسی کردیم. دقیقا همان مراحل، در اینجا نیز باید طی شوند و از این مجموعه تنها به کامپوننت Pagination آن نیاز داریم. همان قسمت ذیل گرید تصویر فوق که شماره صفحات را جهت انتخاب، نمایش داده‌است.
بنابراین ابتدا فرض بر این است که دو بسته‌ی بوت استرپ و ngx-bootstrap را نصب کرده‌اید:
> npm install bootstrap --save
> npm install ngx-bootstrap --save
در فایل angular-cli.json. شیوه‌نامه‌ی بوت استرپ را نیز افزوده‌اید:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
پس از آن باید به‌خاطر داشت که کامپوننت نمایش صفحه بندی این مجموعه PaginationModule نام دارد و باید در نزدیک‌ترین ماژول مورد نیاز، ثبت و معرفی شود:
import { PaginationModule } from "ngx-bootstrap";

@NgModule({
  imports: [
    PaginationModule.forRoot()
  ]
برای نمونه در این مثال، ماژولی به نام simple-grid.module.ts دربرگیرنده‌ی گرید مطلب جاری است و به صورت ذیل به برنامه اضافه شده‌است:
 >ng g m SimpleGrid -m app.module --routing
بنابراین تعریف PaginationModule باید به قسمت imports این ماژول اضافه شود و تعریف آن در app.module.ts تاثیری بر روی این قسمت نخواهد داشت.

کامپوننتی هم که مثال جاری را نمایش می‌دهد به صورت ذیل به ماژول SimpleGrid فوق اضافه شده‌است:
 >ng g c SimpleGrid/products-list


تهیه معادل‌های قراردادهای سمت سرور در سمت Angular

در قسمت قبل، تعدادی قرارداد مانند پارامترهای دریافتی از سمت کلاینت و ساختار اطلاعات ارسالی به سمت کلاینت را تعریف کردیم. اکنون جهت کار strongly typed با آن‌ها در سمت یک برنامه‌ی تایپ اسکریپتی Angular، کلاس‌های معادل آن‌ها را تهیه می‌کنیم.

ساختار شیء محصول دریافتی از سمت سرور
 >ng g cl SimpleGrid/app-product
با این محتوا
export class AppProduct {
  constructor(
    public productId: number,
    public productName: string,
    public price: number,
    public isAvailable: boolean
  ) {}
}
که در اینجا هر کدام از خواص ذکر شده، معادل camel case نمونه‌ی سمت سرور خود هستند (چون JSON.NET در ASP.NET Core، به صورت پیش فرض یک چنین خروجی را تولید می‌کند).

ساختار معادل پارامترهای صفحه بندی و مرتب سازی ارسالی به سمت سرور
 >ng g cl SimpleGrid/PagedQueryModel
با این محتوا
export class PagedQueryModel {
  constructor(
    public sortBy: string,
    public isAscending: boolean,
    public page: number,
    public pageSize: number
  ) {}
}
در اینجا همان ساختار IPagedQueryModel سمت سرور را مشاهده می‌کنید. از آن جهت مشخص سازی جزئیات صفحه بندی و نحوه‌ی مرتب سازی اطلاعات، استفاده می‌شود.

ساختار معادل اطلاعات صفحه بندی شده‌ی دریافتی از سمت سرور
 >ng g cl SimpleGrid/PagedQueryResult
با این محتوا
export class PagedQueryResult<T> {
  constructor(public totalItems: number, public items: T[]) {}
}
این ساختار جنریک نیز دقیقا معادل همان PagedQueryResult سمت سرور است و حاوی تعداد کل ردیف‌های یک کوئری و تنها قسمتی از اطلاعات صفحه بندی شده‌ی آن می‌باشد.

ساختار ستون‌های گرید نمایشی
 >ng g cl SimpleGrid/GridColumn
با این محتوا
export class GridColumn {
  constructor(
    public title: string,
    public propertyName: string,
    public isSortable: boolean
  ) {}
}
هر ستون نمایش داده شده، دارای یک برچسب، خاصیتی مشخص در سمت سرور و بیانگر قابلیت مرتب سازی آن می‌باشد. اگر isSortable به true تنظیم شود، با کلیک بر روی سرستون‌ها می‌توان اطلاعات را بر اساس آن ستون، مرتب سازی کرد.


تهیه سرویس ارسال اطلاعات صفحه بندی به سرور و دریافت اطلاعات از آن

پس از تدارک این مقدمات، اکنون کار تعریف سرویسی که این اطلاعات را به سمت سرور ارسال می‌کند و نتیجه را باز می‌گرداند، به صورت ذیل خواهد بود:
 >ng g s SimpleGrid/products-list -m simple-grid.module
این دستور سبب ایجاد کلاس ProductsListService شده و همچنین قسمت providers ماژول simple-grid را نیز بر این اساس به روز رسانی می‌کند.
پیش از تکمیل این سرویس، نیاز است متدی را جهت تبدیل یک شیء، به معادل کوئری استرینگ آن تهیه کنیم:
  toQueryString(obj: any): string {
    const parts = [];
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        const value = obj[key];
        if (value !== null && value !== undefined) {
          parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
        }
      }
    }
    return parts.join("&");
  }
در قسمت قبل امضای متد GetPagedProducts دارای ویژگی HttpGet است. بنابراین، نیاز است اطلاعات را به صورت کوئری استرینگ از سمت کلاینت دریافت کند و متد toQueryString فوق به صورت خودکار بر روی تمام خواص یک شیء دلخواه حرکت کرده و آن‌ها را تبدیل به یک رشته‌ی حاوی کوئری استرینگ‌ها می‌کند.
[HttpGet("[action]")]
public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
برای نمونه متد toQueryString فوق است که سبب ارسال یک چنین درخواستی به سمت سرور می‌شود:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7

پس از این تعریف، سرویس ProductsListService  به صورت ذیل تکمیل خواهد شد:
@Injectable()
export class ProductsListService {
  private baseUrl = "api/Product";

  constructor(private http: Http) {}

  getPagedProductsList(
    queryModel: PagedQueryModel
  ): Observable<PagedQueryResult<AppProduct>> {
    return this.http
      .get(`${this.baseUrl}/GetPagedProducts?${this.toQueryString(queryModel)}`)
      .map(res => {
        const result = res.json();
        return new PagedQueryResult<AppProduct>(
          result.totalItems,
          result.items
        );
      });
  }
در اینجا از متد toQueryString، جهت تکمیل متد get ارسالی به سمت سرور استفاده شده‌است تا پارامترها را به صورت کوئری استرینگ‌ها تبدیل کرده و ارسال کند.
سپس در متد map آن، res.json دقیقا همان ساختار PagedQueryResult سمت سرور را به همراه دارد. اینجا است که فرصت خواهیم داشت نمونه‌ی سمت کلاینت آن‌را که در ابتدای بحث تهیه کردیم، وهله سازی کرده و بازگشت دهیم (نگاشت فیلدهای دریافتی از سمت سرور به سمت کلاینت).


تکمیل کامپوننت نمایش گرید

قسمت آخر این مطلب، استفاده‌ی از این ساختارها و سرویس‌ها و نمایش اطلاعات دریافتی از آن‌ها است. برای این منظور ابتدا نیاز است سرستون‌های این گرید را تهیه کرد:


  <table class="table table-striped table-hover table-bordered table-condensed">
    <thead>
      <tr>
        <th class="text-center" style="width:3%">#</th>
        <th *ngFor="let column of columns" class="text-center">
          <div *ngIf="column.isSortable" (click)="sortBy(column.propertyName)" style="cursor: pointer">
            {{ column.title }}
            <i *ngIf="queryModel.sortBy === column.propertyName" class="glyphicon"
              [class.glyphicon-sort-by-order]="queryModel.isAscending" [class.glyphicon-sort-by-order-alt]="!queryModel.isAscending"></i>
          </div>
          <div *ngIf="!column.isSortable" style="cursor: pointer">
            {{ column.title }}
          </div>
        </th>
      </tr>
    </thead>
در اینجا ابتدا بررسی می‌شود که آیا یک ستون قابلیت مرتب سازی را دارد، یا خیر؟ اگر اینطور است، در کنار آن یک گلیف آیکن مرتب سازی درج می‌شود. اگر خیر، صرفا متن عنوان آن نمایش داده خواهد شد. می‌شد تمام این موارد را به ازای هر ستون به صورت مجزایی ارائه داد، اما در این حالت به کدهای تکراری زیادی می‌رسیدیم. به همین جهت از یک حلقه بر روی تعریف ستون‌های این گرید استفاده شده‌است. آرایه‌ی این ستون‌ها نیز به صورت ذیل تعریف می‌شود:
export class ProductsListComponent implements OnInit {
  columns: GridColumn[] = [
    new GridColumn("Id", "productId", true),
    new GridColumn("Name", "productName", true),
    new GridColumn("Price", "price", true),
    new GridColumn("Available", "isAvailable", true)
  ];

همچنین در کدهای قالب این کامپوننت، مدیریت کلیک بر روی یک سر ستون را نیز مشاهده می‌کنید:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);

  sortBy(columnName) {
    if (this.queryModel.sortBy === columnName) {
      this.queryModel.isAscending = !this.queryModel.isAscending;
    } else {
      this.queryModel.sortBy = columnName;
      this.queryModel.isAscending = true;
    }
    this.getPagedProductsList();
  }
}
در این‌حالت اگر ستونی که بر روی آن کلیک شده، پیشتر مرتب سازی شده‌است، صرفا خاصیت صعودی بودن آن برعکس خواهد شد. در غیراینصورت، نام خاصیت درخواستی مرتب سازی و جهت آن نیز مشخص می‌شود. سپس مجددا این گرید توسط متد getPagedProductsList رندر خواهد شد.

کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>
اولین ستون آن، اندکی ابتکاری است. در اینجا شماره ردیف‌های خودکاری در هر صفحه درج خواهند شد. این شماره ردیف نیز جزو ستون‌های منبع داده‌ی فرضی برنامه نیست. به همین جهت برای درج آن، توسط let i = index در ngFor، به شماره ایندکس ردیف جاری دسترسی پیدا می‌کنیم. سپس توسط محاسباتی بر اساس تعداد ردیف‌های هر صفحه و شماره‌ی صفحه‌ی جاری، می‌توان شماره ردیف فعلی را محاسبه کرد.

در اینجا حلقه‌ای بر روی queryResult.items تشکیل شده‌است. این منبع داده به صورت ذیل در کامپوننت متناظر مقدار دهی می‌شود:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;
  isLoading = false;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);
  queryResult = new PagedQueryResult<AppProduct>(0, []);

  constructor(private productsListService: ProductsListService) {}

  ngOnInit() {
    this.getPagedProductsList();
  }

  private getPagedProductsList() {
    this.isLoading = true;
    this.productsListService
      .getPagedProductsList(this.queryModel)
      .subscribe(result => {
        this.queryResult = result;
        this.isLoading = false;
      });
  }
}
ابتدا سرویس ProductsListService را که در ابتدای بحث تکمیل شد، به سازنده‌ی این کامپوننت تزریق می‌کنیم. به کمک آن می‌توان در متد getPagedProductsList، ابتدا queryModel جاری را که شامل اطلاعات مرتب سازی و صفحه بندی است، به سرور ارسال کرده و سپس نتیجه‌ی نهایی را به queryResult انتساب دهیم. به این ترتیب تعداد کل رکوردها و همچنین آیتم‌های صفحه‌ی جاری دریافت می‌شوند. اکنون حلقه‌ی ngFor نمایش بدنه‌ی گرید، کار تکمیل صفحه‌ی جاری را انجام خواهد داد.

قسمت آخر کار، افزودن کامپوننت نمایش شماره صفحات است:


  <div align="center">
    <pagination [maxSize]="8" [boundaryLinks]="true" [totalItems]="queryResult.totalItems"
      [rotate]="false" previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;"
      lastText="&raquo;" (numPages)="numberOfPages = $event" [(ngModel)]="currentPage"
      (pageChanged)="onPageChange($event)"></pagination>
  </div>
  <pre class="card card-block card-header">Page: {{currentPage}} / {{numberOfPages}}</pre>
در اینجا از کامپوننت pagination مجموعه‌ی ngx-bootstarp استفاده شده‌است و یک سری از خواص مستند شده‌ی آن‌، مقدار دهی شده‌اند؛ مانند متن‌های صفحه‌ی بعد و قبل و امثال آن. مدیریت کلیک بر روی شماره‌های آن، در کامپوننت جاری به صورت ذیل است:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;

  onPageChange(event: any) {
    this.queryModel.page = event.page;
    this.getPagedProductsList();
  }
}
علت تعریف دو خاصیت اضافه‌ی currentPage و numberOfPages، استفاده‌ی از آن‌ها در قسمت ذیل این شماره‌ها (خارج از کامپوننت نمایش شماره صفحات) جهت نمایش page 1/x است.
هر زمانیکه کاربر بر روی شما‌ره‌ای کلیک می‌کند، رخ‌داد onPageChange فراخوانی شده و در این‌حالت تنها کافی است شماره صفحه‌ی درخواستی queryModel جاری را به روزرسانی کرده و سپس آن‌را در اختیار متد getPagedProductsList جهت دریافت اطلاعات این صفحه‌ی درخواستی قرار دهیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
فعالسازی امکانات Refactoring افزونه‌ی Roslynator در VSCode
یکی از قابلیت‌های افزونه‌ی C# for Visual Studio Code پس از نگارش 1.10.0 آن، امکان بارگذاری افزونه‌های مخصوص Roslyn است که قابلیت‌های Refactoring را به همراه دارند؛ مانند افزونه‌ی معروف و جامع Roslynator. البته هنوز افزونه‌های Analyzers مبتنی بر Roslyn، با VSCode سازگاری ندارند که قرار است در نگارش‌های آتی افزوده شوند. در این مطلب، نحوه‌ی فعالسازی افزونه‌های Roslyn refactoring ثالث را بررسی خواهیم کرد.


فعالسازی قدم به قدم Roslyn refactoring افزونه‌ی Roslynator

الف) فایل VSIX آن‌را از اینجا دریافت کنید و سپس پسوند آن‌را به zip تغییر دهید.
ب) این فایل zip را در پوشه‌ای مشخص باز کنید.
ج) پس از باز کردن این فایل zip، دو فایل Roslynator.VisualStudio.Core.dll و Roslynator.VisualStudio.dll آن‌را حذف کنید. این فایل‌ها مخصوص نگارش کامل ویژوال استودیو هستند و در صورت وجود، با سیستم بارگذاری افزونه‌های OmniSharp تداخل می‌کنند.
د) در آخر مسیر زیر را گشوده:
%USERPROFILE%/.omnisharp
و سپس فایل جدید omnisharp.json را با محتوای ذیل، در آن مسیر ایجاد کنید:
{
  "RoslynExtensionsOptions": {
    "LocationPaths": [
      "C:\\lib\\roslynator"
    ]
  }
}
که در اینجا مسیر ذکر شده، به پوشه‌ای اشاره می‌کند که فایل zip افزونه را در آنجا گشوده‌اید (به ذکر \\ هم دقت داشته باشید؛ تا فایل json نهایی، به درستی تشکیل شود).

اکنون اگر VSCode را اجرا کنید، شاهد افزوده شدن امکانات Refactoring مخصوص افزونه‌ی Roslynator به لیست Refactoring پیش‌فرض OmniSharp خواهید بود:



خودکار سازی دریافت، نصب و فعالسازی Roslyn refactoring افزونه‌ی Roslynator

مراحل فوق را می‌توان تبدیل به یک اسکریپت پاورشل کرد که با هر بار اجرای آن، به صورت خودکار کار دریافت و نصب این افزونه صورت گیرد:
Write-Host "Download, unzip and enable Roslynator for Visual Studio Code"

$name = "josefpihrt.Roslynator2017"
$url = "https://marketplace.visualstudio.com/items?itemName=$name"
$currentDir = $PSScriptRoot
$file = "$currentDir\Roslynator.zip"

$pattern = "<script class=`"vss-extension`" defer=`"defer`" type=`"application\/json`">(.*?)<\/script>"
$regex = [regex]"(?m)$pattern"
Write-Host "Grab the home page of the $name."
$dom = (New-Object Net.WebClient).DownloadString($url); 
if($dom -and $dom -match $pattern) 
{
    $matches = $regex.Match($dom)
    $jsonText = $matches[0].Groups[1]

    $json = ConvertFrom-Json $jsonText

    $version = $Json.versions[0].version # Parse the json in the page for the latest version number
    $parts = $name.Split(".")
    $publisher = $parts[0]
    $package = $parts[1]

    # Assemble the url for the vsix package
    $packageUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/$publisher/vsextensions/$package/$version/vspackage"
    Write-Host "Download the vsix package: $packageUrl"
    (New-Object Net.WebClient).DownloadFile($packageUrl, $file)

    Write-Host "Using $currentDir as the current dir."
    Write-Host "Unzip $file."
    $shellApp = new-object -com shell.application 
    $zipFile = $shellApp.namespace($file) 
    $destination = $shellApp.namespace($currentDir) 
    $destination.Copyhere($zipFile.items(), 0x14)# overwrite and be silent

    Write-Host "Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp."
    Remove-Item "$currentDir\Roslynator.VisualStudio.Core.dll","$currentDir\Roslynator.VisualStudio.dll", "$currentDir\Roslynator.VisualStudio.pkgdef"

    $omnisharpJsonFilePath = "$env:USERPROFILE\.omnisharp\omnisharp.json";
    Write-Host "Create $omnisharpJsonFilePath file."
    $omnisharpJson = @" 
{{
  "RoslynExtensionsOptions": {{
    "LocationPaths": [
      "{0}"
    ]
  }}
}}
"@ -f $currentDir -Replace "\\","\\"
    $omnisharpJson | Out-File "$omnisharpJsonFilePath" -Confirm

    Write-Host "Done!"
}
else
{
    Write-Host "Failed to find the packageUrl!"
}
کدهای فوق را با نام فرضی update.ps1 ذخیره کنید (و یا از اینجا دریافت کنید: update.zip ). سپس می‌توانید آن‌را با اجرای دستور update.ps1\.، اجرا کرده و به صورت خودکار شاهد این مراحل باشید:
PS D:\Prog\1396\RoslynatorVSCode> .\update.ps1
Download, unzip and enable Roslynator for Visual Studio Code
Grab the home page of the josefpihrt.Roslynator2017.
Download the vsix package: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/josefpihrt/vsextensions/Roslynator2017/1.7.0/vspackage
Using D:\Prog\1396\RoslynatorVSCode as the current dir.
Unzip D:\Prog\1396\RoslynatorVSCode\Roslynator.zip.
Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp.
Create C:\Users\Vahid\.omnisharp\omnisharp.json file.

Confirm
Are you sure you want to perform this action?
Performing the operation "Output to File" on target "C:\Users\Vahid\.omnisharp\omnisharp.json".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):
Done!
ابتدا صفحه‌ی اصلی این افزونه دریافت می‌شود. سپس از داخل آن، پارامترهای مخصوص دانلود افزونه استخراج و آدرس دریافت نهایی آن تشکیل می‌شود.
در ادامه این افزونه دریافت شده و در پوشه‌ی جاری باز خواهد شد. سپس فایل omnisharp.json نیز به صورت خودکار تشکیل و مقدار دهی می‌شود.
اکنون اگر VSCode را اجرا کنید، همه چیز آماده بوده و امکانات این افزونه در دسترس خواهند بود.
اشتراک‌ها
پروژه ApiBoilerPlate برای ساخت پروژه های ASP.NET Core APIs

A simple yet organized project template for building ASP.NET Core APIs in .NET Core 3.1

Tools and Frameworks Used

  • .NET Core 3.1
  • ASP.NET Core - For building RESTful APIs
  • Dapper - For data access.
  • AutoMapper - For mapping entity models to DTOs.
  • AutoWrapper - For handling request Exceptions and consistent HTTP response format.
  • AutoWrapper.Server - For unwrapping the Result attribute from AutoWrapper's ApiResponse output.
  • Swashbuckle.AspNetCore - For securing API documentation.
  • FluentValidation.AspNetCore - For Model validations
  • Serilog.AspNetCore - For logging capabilities
  • IdentityServer4.AccessTokenValidation - For JWT Authentication handling
  • Microsoft.Extensions.Http.Polly - For handling HttpClient Resilience and Transient fault-handling
  • AspNetCoreRateLimit - For controlling the rate of requests that clients can make to an external API based on IP address or client ID.
  • AspNetCore.Diagnostics.HealthChecks - For performing health checks
  • Microsoft.AspNetCore.Diagnostics.HealthChecks - For getting the results of Health Checks in the application
  • AspNetCore.HealthChecks.UI - For Health Status visualization
  • xUnit and Moq - For unit testing.  


پروژه ApiBoilerPlate برای ساخت پروژه های ASP.NET Core APIs
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
تغییرات پردازش تاریخ از زمان ارائه‌ی ASP.NET Core 3.1

از زمان ارائه‌ی کتابخانه‌ی JSON توکار در ASP.NET Core 3x، نحوه‌ی پردازش تاریخ‌های دریافتی در برنامه‌های ASP.NET Core نیز تغییر کرد و در حالت دریافت اطلاعات به صورت JSON در اکشن متدها، فقط بر اساس ISO 8601-1:2019 عمل می‌کند. یعنی اگر تاریخ را با فرمت متداول 2019/06/14 به سمت سرور ارسال کنید، خطای ModelState زیر را دریافت خواهید کرد:
{
    "$.time": [
        "The JSON value could not be converted to System.DateTime. Path: $.time | LineNumber: 1 | BytePositionInLine: 23."
    ]
}
که عنوان می‌کند تاریخ ارسالی قابل پردازش نیست. چون فرمت دریافتی آن باید به این صورت باشد:
2019-06-14T02:30:04.0576719Z
و در کل این فرمت‌ها را قبول می‌کند:
'Full date'

    "yyyy'-'MM'-'dd"

"'Full date''T''Hour'':''Minute'"

    "yyyy'-'MM'-'dd'T'HH':'mm"

"'Full date''T''Partial time'"

    "yyyy'-'MM'-'dd'T'HH':'mm':'ss" (The Sortable ("s") Format Specifier)
    "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF"

"'Full date''T''Time hour'':''Minute''Time offset'"

    "yyyy'-'MM'-'dd'T'HH':'mmZ"
    "yyyy'-'MM'-'dd'T'HH':'mm('+'/'-')HH':'mm"

'Date time'

    "yyyy'-'MM'-'dd'T'HH':'mm':'ssZ"
    "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFFZ"
    "yyyy'-'MM'-'dd'T'HH':'mm':'ss('+'/'-')HH':'mm"
    "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'FFFFFFF('+'/'-')HH':'mm"


تغییرات پردازش تاریخ در ASP.NET Core 5x

در نگارش‌های قبلی ASP.NET Core، اگر تاریخی از طریق کوئری استرینگ و با فرمت ISO فوق دریافت می‌شد، مانند:
https://localhost:44393/time?time=2019-06-14T02:30:04.0576719Z
زمانیکه در سمت سرور پردازش می‌شد، نوع آن به اشتباه به Local ترجمه می‌شد و UTC آن درنظر گرفته نمی‌شد:
Model-bound value is: 2019-06-13T19:30:04.0576719-07:00, Kind is: Local
این مشکل در ASP.NET Core 5x برطرف شده‌است و اینبار Kind تاریخ پردازش شده، از نوع UTC ارسالی است.

یک نکته: ویژگی‌های BindProperty و DisplayFormat در razor pages، امکان تغییر فرمت ورودی و نمایشی را می‌دهند:
[BindProperty, DisplayFormat(DataFormatString = "{0:yyyy-MM-ddTHH:mm}", ApplyFormatInEditMode = true)]
public DateTime DateTime { get; set; }

DateTime: <input asp-for="DateTime"  asp-format="{0:yyyy-MM-ddTHH:mm}" />
اشتراک‌ها
لیست تغییرات جدید ASP.NET Core 5.0

عناوین برخی از امکانات و بهبود ها

  • MVC model binding improvements, including support for C# 9 record types
  • Blazor Server & Blazor WebAssembly support and improvements
  • Built-in OpenAPI and Swagger UI support for Web APIs
  • SignalR Hub filters and parallel Hub invocations
  • Azure AD authentication with MIcrosoft.Identity.Web
  • Auto browser refresh with dotnet watch
  • HTTP/2 and gRPC performance improvements 
لیست تغییرات جدید ASP.NET Core 5.0