مسیرراه‌ها
React 16x
پیش نیاز ها
کامپوننت ها
ترکیب کامپوننت ها
طراحی یک گرید
مسیریابی 
کار با فرم ها
ارتباط با سرور
احراز هویت و اعتبارسنجی کاربران 
React Hooks  
توزیع برنامه

مدیریت پیشرفته‌ی حالت در React با Redux و Mobx   

       Redux
       MobX  

مطالب تکمیلی 
    مطالب
    طراحی یک گرید با 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 جهت دریافت اطلاعات این صفحه‌ی درخواستی قرار دهیم.


    کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
    مطالب
    میان‌افزار جدید Authorization در ASP.NET Core 3.0
    در نگارش‌های اولیه‌ی ASP.NET Core، پشتیبانی از authorization، صرفا توسط ویژگی [Authorize]، قابل اعمال به اکشن متد خاصی بود. برای بهبود تنظیم این قابلیت، میان‌افزار جدید Authorization به ASP.NET Core 3.0 اضافه شده‌است و تنظیم آن جهت کار با امکانات امنیتی برنامه، الزامی است؛ در غیر اینصورت در حین مرور این صفحات و قسمت‌های محافظت شده، برنامه با خطای زیر متوقف خواهد شد:
    Endpoint xyz contains authorization metadata, but a middleware was not found that supports authorization.
    Configure your application startup by adding app.UseAuthorization() inside the call to Configure(..) in the application startup code.


    محل صحیح تعریف میان‌افزار Authorization در ASP.NET Core 3.0

    توصیه شده‌است میان‌افزار جدید Authorization، با فراخوانی متد UseAuthorization، بلافاصله پس از فراخوانی متد UseAuthentication معرفی شود. در این حالت این میان‌افزار، با یک Policy پیش‌فرض تنظیم می‌شود که قابل تغییر و بازنویسی است:
    public void Configure(IApplicationBuilder app)
    {
        ...
    
        app.UseRouting();
    
        app.UseAuthentication();
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
        });
    }
    در مورد مفهوم متد MapDefaultControllerRoute، می‌توانید به نظرات تکمیلی مطلب Endpoint routing مراجعه کنید.
    در ASP.NET Core 3.0، پس از تنظیمات فوق است که قطعه کد زیر و اعمال فیلتر Authorize، مجددا کار می‌کند:
    public class HomeController : ControllerBase
    {
        [Authorize]
        public IActionResult BuyWidgets()
        {
            ...
        }
    }


    روش تعریف فیلتر Authorize به صورت سراسری در ASP.NET Core 3.0

    می‌توان AuthorizeFilter را به صورت یک فیلتر سراسری (global filter in MVC) تعریف کرد تا به تمام کنترلرها و اکشن متدها اعمال شود. هرچند این روش هنوز هم در ASP.NET Core 3.0 کار می‌کند، اما روش توصیه شده‌ی جدید آن به صورت زیر است:
    public void Configure(IApplicationBuilder app)
    {
        ...
    
        app.UseRouting();
    
        app.UseAuthentication();
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute().RequireAuthorization();
        });
    }
    در این حالت با اعمال RequireAuthorization به MapDefaultControllerRoute، سبب خواهیم شد تا اعتبارسنجی کاربر برای استفاده‌ی از تمام قسمت‌های برنامه، اجباری شود. در این بین اگر کنترلری و یا اکشن متدی نباید محافظت شود و دسترسی آزادانه‌ی به آ‌ن‌ها مدنظر است، می‌توان از فیلتر AllowAnonymous استفاده کرد:
    [AllowAnonymous]
    public class HomeController : ControllerBase
    {
        ...
    }


    سفارشی سازی سیاست‌های Authorization در ASP.NET Core 3.0

    اگر بخواهیم DefaultPolicy میان‌افزار Authorization را بازنویسی کنیم، می‌توان از متد services.AddAuthorization به صورت زیر استفاده کرد که در آن authentication و داشتن یک Scope خاص را اجباری می‌کند:
    public void ConfigureServices(IServiceCollection services)
    {
        ...
        
        services.AddAuthorization(options =>
        {
            options.DefaultPolicy = new AuthorizationPolicyBuilder()
              .RequireAuthenticatedUser()
              .RequireScope("MyScope")
              .Build();
        });
    }


    تنظیم سیاست‌های دسترسی، زمانیکه هیچ نوع تنظیمی از پیش تعریف نشده‌است

    با تنظیم FallbackPolicy، می‌توان تمام endpointهایی را که توسط فیلتر [Authorize] و یا ()RequireAuthorization محافظت نشده‌اند، محافظت کرد.
    DefaultPolicy توسط فیلتر [Authorize] و یا ()RequireAuthorization مورد استفاده قرار می‌گیرد؛ اما FallbackPolicy زمانی بکار خواهد رفت که هیچ نوع سیاست دسترسی تنظیم نشده‌باشد. حالت پیش‌فرض FallbackPolicy، پذیرش تمام درخواست‌ها بدون اعتبارسنجی است.
    کارکرد مثال زیر با مثال‌های قبلی که از DefaultPolicy استفاده می‌کردند، یکی است و هدف آن، اجبار به اعتبارسنجی کاربران در همه‌جا (تمام endpoints)، منهای مواردی است که از فیلتر AllowAnonymous استفاده می‌شود:
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthorization(options =>
        {
            options.FallbackPolicy = new AuthorizationPolicyBuilder()
              .RequireAuthenticatedUser()
              .RequireScope("MyScope")
              .Build();
        });
    }
    
    public void Configure(IApplicationBuilder app)
    {
        app.UseRouting();
    
        app.UseAuthentication();
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
        });
    }
    
    [AllowAnonymous]
    public class HomeController : ControllerBase
    {
    }


    امکان اعمال سفارشی میان‌افزار Authorization به Endpointهای مجزا

    برای مثال در مبحث «بررسی سلامت برنامه»، فریم‌ورک، اعتبارسنجی درخواست‌های آن‌را پیش بینی نکرده‌است؛ اما اینبار می‌توان اعتبارسنجی را به endpoint آن اعمال کرد:
    public void Configure(IApplicationBuilder app)
    {
        ...
    
        app.UseRouting();
    
        app.UseAuthentication();
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints
                .MapHealthChecks("/healthz")
                .RequireAuthorization(new AuthorizeAttribute(){ Roles = "admin", });
        });
    }
    همانطور که مشاهده می‌کنید، در اینجا RequireAuthorization، به صورت سفارشی به یک endpoint خاص اعمال شده‌است و تنظیمات آن، با تنظیمات سایر قسمت‌های برنامه یکی نیست.
    نظرات اشتراک‌ها
    کتابخانه ImageProcessor Core
    تمام NUget‌های ImageProcessor برای net framework هست
    برای dotnet core از کدوم nuget باید استفاده کرد؟