نگاهی به Duende IdentityServer 5
Securing your application is bloody important. With so much jargon to sift through, it’s easy to get lost, for example there’s SSO, OAuth2, SAML 2.0, OpenID Connect, Federated Identity, 2FA, & MFA. Just to name a few! 😱 In this talk, Anthony will take an in depth look at Federated Identity using OpenID Connect and OAuth2 Framework for ASP. NET Core using Duende IdentityServer (aka IdentityServer 5). You will walk away knowing how to navigate the security options and avoid the madness.
«بررسی روش آپلود فایلها در ASP.NET Core»
«ارسال فایل و تصویر به همراه دادههای دیگر از طریق jQuery Ajax»
- در مطلب اول، روش دریافت فایلها از کلاینت، در سمت سرور و ذخیره سازی آنها در یک برنامهی ASP.NET Core بررسی شدهاست که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شدهاست. هرچند در مطلب جاری از jQuery استفاده نمیشود، اما نکات نحوهی کار با شیء FormData استاندارد، در اینجا نیز یکی است.
تدارک مقدمات مثال این قسمت
این مثال در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید UploadFile را به آن اضافه میکنیم:
>ng g m UploadFile -m app.module --routing
>ng g c UploadFile/UploadFileSimple
در ادامه کلاس مدل معادل فرم ثبت نام یک درخواست پشتیبانی را تعریف میکنیم:
>ng g cl UploadFile/Ticket
export class Ticket { constructor(public description: string = "") {} }
ایجاد مقدمات کامپوننت UploadFileSimple و قالب آن
پس از ایجاد ساختار کلاس Ticket، یک وهله از آنرا به نام model ایجاد کرده و در اختیار قالب آن قرار میدهیم:
import { Ticket } from "./../ticket"; export class UploadFileSimpleComponent implements OnInit { model = new Ticket();
<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>
سپس در انتها، فیلد آپلود را مشاهده میکنید؛ با این ویژگیها:
الف) 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); }
در اینجا ساختار شیء استاندارد FileList و اجزای آنرا مشاهده میکنید. برای مثال چون دو فایل انتخاب شدهاست، این لیست به همراه یک خاصیت طول و دو شیء File است.
تعاریف این اشیاء استاندارد، در فایل ذیل قرار دارند و به همین جهت است که VSCode، بدون نیاز به تنظیمات دیگری، آنها را شناسایی و intellisense متناظری را مهیا میکند:
C:\Program Files (x86)\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.dom.d.ts
{ "lib": [ "es2016", "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 کامپوننت، به متغیری به همین نام در قالب متناظر با آن متصل شدهاست. بنابراین با استفاده از خاصیت nativeElement آن همانند کدهایی که در متد submitForm فوق ملاحظه میکنید، میتوان به خاصیت files این کنترل ارسال فایلها دسترسی یافت.
نوع جدید و استاندارد HTMLInputElement نیز در فایل lib.dom.d.ts که پیشتر معرفی شد، ثبت شدهاست.
ارسال فرم درخواست پشتیبانی به سرور
تا اینجا فرمی را تشکیل داده و همچنین به فیلد file آن دسترسی پیدا کردیم. اکنون میخواهیم این اطلاعات را به سمت سرور ارسال کنیم. برای این منظور، سرویس جدیدی را ایجاد خواهیم کرد:
>ng g s UploadFile/UploadFileSimple -m upload-file.module
در ادامه کدهای کامل این سرویس را مشاهده میکنید:
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]); } }
ب) افزودن فایلها به شیء FormData
پس از افزودن اطلاعات ticket به FormData، اکنون نوبت به افزودن فایلهای فرم است:
for (let i = 0; i < filesList.length; i++) { formData.append(filesList[i].name, filesList[i]); }
یک نکته: چون در اینجا کلید اضافه شده، نام فایل است، دیگر نمیتوان در سمت سرور از روش 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(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); }); }
دریافت فرم درخواست پشتیبانی در سمت سرور و ذخیرهی فایلهای آن
کدهای کامل SimpleUpload که در سرویس فوق مشخص شدهاست، به صورت ذیل هستند. ابتدا مدل Ticket مشخص شدهاست:
namespace AngularTemplateDrivenFormsLab.Models { public class Ticket { public int Id { set; get; } public string Description { set; get; } } }
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]);
var files = Request.Form.Files; foreach (var file in files)
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
Spawn an Online Game with Blazor, .NET 7 and Clean Architecture in under 60 minutes
In this fun talk, Luke Parker will show you step-by-step how to build a playable game in under 60 minutes. He will go over some basic game design, project planning, then build the game and finally… play live with the audience! Utilizing code sharing and the magic of Clean Architecture allows for very rapid app development. Also, leveraging MudBlazor as the component library lets you save time and to focus on what really matters.
In short, we’re making Xamarin’s cross-platform tools available in
Visual Studio, from Community up through Enterprise at no additional
cost and we’re open-sourcing the Xamarin SDK – the core of the Xamarin
toolchain. In addition, we’re making Xamarin’s services (for example,
Test Cloud and Xamarin University) available along with the existing
Microsoft mobile DevOps capabilities.
https://blog.xamarin.com/xamarin-for-all/?utm_medium=social&utm_campaign=blog&utm_source=twitter&utm_content=xamarin-for-all
کلاسهای CSS اعتبارسنجی در Angular
زمانیکه Angular فرمی را تحت نظر قرار میدهد، کلاسهای CSS خاصی را نیز بر اساس حالات عناصر مختلف آن، به آنها متصل خواهد کرد. بر این اساس میتوان ظاهر این المانها را سفارشی سازی نمود. این کلاسها به شرح زیر هستند:
کلاس CSS اعتبارسنجی | توضیحات |
ng-untouched | زمانیکه فرمی برای بار اول رندر میشود، تمام فیلدهای آن با کلاس CSS ایی به نام ng-untouched علامتگذاری میشوند. |
ng-touched | همینقدر که کاربر با یک Tab از فیلدی عبور کند، با کلاس ng-touched مزین خواهد شد. بنابراین مهم نیست که حتما دادهای وارد شده باشد یا خیر. حتی عبور از یک فیلد نیز به معنای لمس آن نیز میباشد. |
ng-pristine | مربوط به زمانیاست که یک فیلد نه تغییر کردهاست و نه لمس شدهاست. |
ng-dirty | همینقدر که کاربر، تغییری را در فیلدی ایجاد کند، آن المان با کلاس ng-dirty مشخص خواهد شد. |
ng-valid | برای حالت موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده میشود. |
ng-invalid | برای حالت غیر موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده میشود. |
برای اینکه بتوانیم این موارد را در عمل مشاهده کنیم، به ابتدای فرم مثال این سری، تغییرات ذیل را اعمال خواهیم کرد:
<div class="form-group"> <label>First Name</label> <input #firstName required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName"> </div> <h3>Classes</h3> <h4>{{ firstName.className }}</h4>
تصویر فوق کلاسهایی را نمایش میدهد که در اولین بار نمایش فرم، به المان firstName متصل شدهاند. برای مثال در این حالت کلاس ng-pristine قابل مشاهدهاست و هنوز تغییری در آن حاصل نشدهاست.
در ادامه اگر حرفی را به آن اضافه کنیم:
هنوز هم ng-untouched آن برقرار است؛ اما ng-pristine آن به ng-dirty تبدیل شدهاست. در اینجا حتی اگر کل اطلاعات فیلد را نیز حذف کنیم و آنرا خالی کنیم یا به حالت اول بازگردانیم نیز کلاس ng-dirty قابل مشاهدهاست. بنابراین اگر حالت فیلدی dirty شد، همواره به همین حالت باقی میماند.
در این لحظه اگر با Tab به فیلد دیگری در فرم مراجعه کنیم:
در اینجا است که کلاس ng-untouched به ng-touched تبدیل میشود. بنابراین کلاسهای مختلف لمس یک فیلد، ارتباطی به افزوده شدن یا حذف کاراکتری از یک فیلد ندارند و فقط به از دست رفتن focus و مراجعهی به فیلد دیگری مرتبط میشوند.
اگر به المان تغییر یافتهی فوق دقت کنید، ویژگی required نیز به آن اضافه شدهاست (علاوه بر template reference variable ایی که تعریف کردیم). در این حالت کل فیلد را خالی کنید:
همانطور که مشاهده میکنید، اکنون کلاس ng-valid به کلاس ng-invalid تغییر یافتهاست.
ارتباط بین کلاسهای CSS اعتبارسنجی و خواص ngModel
تمام کلاسهای -ng ایی که در بالا معرفی شدند، معادلهای خواص ngModel ایی نیز دارند. فقط کافی است -ng آنها را حذف کنید، باقیماندهی آن، نام خاصیت متناظری در ngModel خواهد بود. برای مثال کلاس ng-untouched به خاصیت untouched نگاشت میشود و به همین ترتیب برای مابقی.
template reference variable ایی را که تا به اینجا به المان اضافه کردیم (firstName#) به خواص همان المان دسترسی دارد (مانند className آن). اما در ادامه میخواهیم این متغیر به ngModel و خواص آن دسترسی داشته باشد و میدان دید آن تغییر کند. به همین جهت تنها کافی است تا ngModel را به این متغیر انتساب دهیم:
<div class="form-group"> <label>First Name</label> <input #firstName="ngModel" required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName"> </div> <h4>dirty: {{ firstName.dirty }}</h4>
یک نکته: در حالت خواص valid و invalid که مرتبط با اعتبارسنجی هستند، خاصیت سومی نیز به نام errors وجود دارد که حاوی اطلاعات بیشتری در مورد خطای اعتبارسنجی رخ دادهاست. بنابراین وجود این خاصیت و نال نبودن آن نیز دلالت بر وجود یک خطای اعتبارسنجی است. از خاصیت errors در ادامهی بحث در قسمت «مدیریت چندین خطای همزمان اعتبارسنجی» استفاده خواهیم کرد.
نمایش بهتر خطاهای اعتبارسنجی با بررسی خواص ngModel
یکی از مزایای کار با خواص ngModel، امکان استفادهی از آنها در عبارات شرطیاست که نسبت به کلاسهای CSS معرفی شدهی در ابتدای بحث، انعطاف پذیری بیشتری را به همراه خواهند داشت.
<div class="form-group"> <label>First Name</label> <input #firstName="ngModel" #firstNameElement required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName"> <div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger"> First Name is required. </div> </div> <h4>className: {{ firstNameElement.className }}</h4> <h4>dirty: {{ firstName.dirty }}</h4> <h4>invalid: {{ firstName.invalid }}</h4>
نمایش بهتر خطاهای اعتبارسنجی با مزین ساختن المانهای ورودی
علاوه بر نمایش یک alert بوت استرپی متناظر با یک فیلد غیرمعتبر، میتوان خود المانهای ورودی را نیز با شیوهنامههایی مزین ساخت.
این کار را در بوت استرپ با افزودن کلاس has-error در کنار form-group انجام میدهند. همچنین label نیز باید به کلاس control-label مزین شود تا hass-error بر روی آن نیز تاثیرگذار شود. برای پیاده سازی پویای آن در Angular به روش ذیل عمل میشود:
<div class="form-group" [class.has-error]="firstName.invalid && firstName.touched"> <label class="control-label">First Name</label>
بنابراین اگر المان firstName خالی باشد و همچنین با یک Tab از روی آن عبور کرده باشیم، کلاس has-error در کنار کلاس form-group اضافه میشود.
روش دوم: همانطور که در ابتدای بحث نیز عنوان شد، Angular بر اساس حالات مختلف یک فیلد، کلاسهای CSS خاصی را به آنها انتساب میدهد. یک چنین کاری را با مقدار دهی این کلاسها در فایل src\styles.css نیز میتوان انجام داد که دقیقا معادل بررسی خواص invalid و touched با کدنویسی است:
.ng-touched.ng-invalid{ border: 1px solid red; }
سایر ویژگیهای اعتبارسنجی HTML 5
تا اینجا ویژگی استاندارد required را به المان ورودی فرم ثبت اطلاعات کاربران، اضافه کردیم.
<input #firstName="ngModel" #firstNameElement required maxlength="3" minlength="2" pattern="^V.*" type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
برای نمونه minlength همهجا پشتیبانی نمیشود؛ اما آنرا میتوان برای مثال با الگویی مساوی "+..." جایگزین کرد.
مشکل! ذکر چند ویژگی اعتبارسنجی با هم، تداخل ایجاد میکنند!
در اینجا چون چهار ویژگی مختلف را به صورت یکجا به یک المان متصل کردهایم، اکنون div ذیل به هر کدام از این ویژگیها به صورت یکسانی واکنش نشان خواهد داد؛ زیرا خاصیت invalid را true میکنند:
<div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger"> First Name is required. </div>
<div class="form-group" [class.has-error]="firstName.invalid && firstName.touched"> <label class="control-label">First Name</label> <input #firstName="ngModel" #firstNameElement required maxlength="3" minlength="2" pattern="^V.*" type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName"> <div *ngIf="firstName.invalid && firstName.touched"> <div class="alert alert-info"> errors: {{ firstName.errors | json }} </div> <div class="alert alert-danger" *ngIf="firstName.errors.required"> Name is required. </div> <div class="alert alert-danger" *ngIf="firstName.errors.minlength"> Name should be minimum {{firstName.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="firstName.errors.maxlength"> Name should be max {{firstName.errors.maxlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="firstName.errors.pattern"> Name pattern: {{firstName.errors.pattern.requiredPattern}} </div> </div> </div>
همانطور که در تصویر ملاحظه میکنید، محتوای خاصیت errors به صورت JSON، جهت دیباگ نیز درج شدهاست.
بنابراین وجود خاصیت firstName.errors.minlength و یا firstName.errors.pattern به این معنا است که این خطاهای خاص وجود دارند (خاصیت firstName.errors به همراه اضافاتی است) و برعکس نال بودن آنها مؤید عدم وجود خطایی است. به همین جهت میتوان به ازای هر کدام، یک div جداگانه را تشکیل داد.
مرحلهی بعد، استخراج اطلاعات بیشتری از همین شیء و محتوای خاصیت errors است. برای مثال زمانیکه در آن خاصیت minlength ظاهر شد، این خاصیت نیز دارای خاصیتی مانند requiredLength است که از آن میتوان جهت درج عدد واقعی مورد نیاز این اعتبارسنج استفاده کرد.
بهبود اعتبارسنجی drop down
در حالت فعلی تعریف drop down مثال این سری، نیازی به اعتبارسنجی نیست؛ چون لیست مشخصی از طریق کامپوننت در اختیار این المان قرار میگیرد و همواره دقیقا یکی از این عناصر انتخاب خواهند شد. اما اگر گزینهی دیگری را مانند:
<option value="default">Select a Language...</option>
<div class="form-group" [class.has-error]="hasPrimaryLanguageError"> <label class="control-label">Primary Language</label> <select class="form-control" name="primaryLanguage" #primaryLanguage (blur)="validatePrimaryLanguage(primaryLanguage.value)" (change)="validatePrimaryLanguage(primaryLanguage.value)" [(ngModel)]="model.primaryLanguage"> <option value="default">Select a Language...</option> <option *ngFor="let lang of languages"> {{ lang }} </option> </select> </div>
export class EmployeeRegisterComponent { hasPrimaryLanguageError = false; validatePrimaryLanguage(value) { if (value === 'default'){ this.hasPrimaryLanguageError = true; } else{ this.hasPrimaryLanguageError = false; } } }
اعتبارسنجی در سطح کل فرم
تا اینجا بررسیهایی را که انجام دادیم، در سطح فیلدها بودند. اکنون اگر کاربر به طور کامل تمام این فیلدها را تغییر ندهد و بر روی دکمهی ارسال کلیک کند چطور؟
<form #form="ngForm" novalidate>
<h3> form.valid: {{ form.valid }}</h3>
<button class="btn btn-primary" type="submit" [disabled]="form.invalid">Ok</button>
در قسمت بعد نحوهی ارسال این فرم را به سرور، بررسی میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
کامپوننت mat-table
کار کامپوننت mat-table نمایش اطلاعات در ردیفها و ستونها است. به همراه آن mat-paginator برای نمایش UI صفحه بندی اطلاعات، دایرکتیو matSort و mat-sort-header برای افزودن رابط کاربری مرتب سازی اطلاعات و امکان تغییر منبع داده آن برای فیلتر کردن دادهها، نیز وجود دارند.
افزودن کامپوننت جدید notes برای نمایش یادداشتهای کاربران
برای نمایش لیست یادداشتهای هر شخص، کامپوننت جدید Notes را به صورت زیر در پوشهی components ایجاد میکنیم:
ng g c contact-manager/components/notes --no-spec
<mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <mat-tab label="Notes"> <app-notes [notes]="user.userNotes"></app-notes> </mat-tab> </mat-tab-group>
import { Component, Input, OnInit } from "@angular/core"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[];
<p> {{notes | json}} </p>
تکمیل کامپوننت Notes توسط یک data table
در ادامه قصد داریم این اطلاعات خام را توسط یک data table نمایش دهیم. به همین جهت ابتدا به مستندات mat-table مراجعه کرده و همانند قبل، مثالی را پیدا میکنیم که به منظور ما نزدیکتر باشد. سپس کدهای آنرا به برنامه اضافه کرده و سفارشی سازی میکنیم. در ابتدا مثال basic آنرا دقیقا به همان نحوی که هست کپی کرده و سپس آنرا تغییر میدهیم:
محتوای فایل notes.component.ts
import { Component, Input, OnInit } from "@angular/core"; import { MatTableDataSource } from "@angular/material"; import { UserNote } from "../../models/user-note"; @Component({ selector: "app-notes", templateUrl: "./notes.component.html", styleUrls: ["./notes.component.css"] }) export class NotesComponent implements OnInit { @Input() notes: UserNote[]; displayedColumns = ["position", "title", "date"]; dataSource: MatTableDataSource<UserNote>; constructor() { } ngOnInit() { this.dataSource = new MatTableDataSource<UserNote>(this.notes); } }
سپس این منبع داده در قسمت ngOnInit بر اساس ورودی آرایهی notes که از کامپوننت main-content مقدار دهی میشود، تامین خواهد شد.
displayedColumns نیز لیست ستونها را مشخص میکند.
محتوای فایل notes.component.html
<div class="example-container mat-elevation-z8" fxLayout="column"> <mat-table #table [dataSource]="dataSource"> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef> No. </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.id}} </mat-cell> </ng-container> <ng-container matColumnDef="title"> <mat-header-cell *matHeaderCellDef> Title </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.title}} </mat-cell> </ng-container> <ng-container matColumnDef="date"> <mat-header-cell *matHeaderCellDef> Date </mat-header-cell> <mat-cell *matCellDef="let note"> {{note.date | date:'yyyy-MM-dd'}} </mat-cell> </ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row> </mat-table> </div>
سپس به ازای هر ستون، یک ng-container اضافه شدهاست. matColumnDef معادل نامهای displayedColumns خواهد بود. matCellDef نیز بر اساس متغیر حلقهای که بر روی منبع داده تشکیل میشود، تعریف خواهد شد. این تعریف امکان دسترسی به مقدار آنرا در ادامه میسر میکند.
در این حالت اگر برنامه را اجرا کنیم، خروجی زیر قابل مشاهده خواهد بود:
افزودن صفحه بندی به mat-table یادداشتهای یک کاربر
اگر مجددا به مستندات mat-table مراجعه کنیم، مثالی در مورد mat-paginator نیز دارد که جهت نمایش رابط کاربری صفحه بندی مورد استفاده قرار میگیرد. بنابراین از مثال آن جهت تکمیل این قسمت ایده میگیریم:
</mat-table> <mat-paginator #paginator [pageSize]="2" [pageSizeOptions]="[2, 4, 6]"> </mat-paginator> </div>
در ادامه به کدهای کامپوننت مراجعه کرده و توسط ViewChild به template reference variable ایی به نام paginator دسترسی پیدا میکنیم:
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatPaginator) paginator: MatPaginator; ngAfterViewInit() { this.dataSource.paginator = this.paginator; } }
اکنون اگر برنامه را اجرا کنیم، صفحه بندی فعال شدهاست:
افزودن جستجو و فیلتر کردن اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال filtering نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
ابتدا فیلد ورود اطلاعات جستجو، پیش از Mat-table به قالب کامپوننت اضافه میشود:
<div class="example-container mat-elevation-z8" fxLayout="column"> <div class="example-header"> <mat-form-field> <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter"> </mat-form-field> </div>
applyFilter(filterValue: string) { this.dataSource.filter = filterValue.trim().toLowerCase(); // MatTableDataSource defaults to lowercase matches }
افزودن مرتب سازی اطلاعات به mat-table یادداشتهای یک کاربر
مستندات mat-table به همراه مثال sorting نیز هست که از آن جهت تکمیل این قسمت به نحو ذیل ایده خواهیم گرفت:
برای فعالسازی مرتب سازی اطلاعات، در قالب کامپوننت، به mat-table، دایرکتیو matSort و به هر ستونی که نیاز است مرتب سازی شود، دایرکتیو mat-sort-header را به mat-headerها اضافه میکنیم:
<mat-table #table [dataSource]="dataSource" matSort> <ng-container matColumnDef="position"> <mat-header-cell *matHeaderCellDef mat-sort-header> No. </mat-header-cell>
export class NotesComponent implements OnInit, AfterViewInit { dataSource: MatTableDataSource<UserNote>; @ViewChild(MatSort) sort: MatSort; ngAfterViewInit() { this.dataSource.sort = this.sort; } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-04.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.