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



افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard

در اینجا قصد داریم صفحه‌ای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شده‌ی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شده‌ی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه می‌کنیم:
 >ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component";

const routes: Routes = [
  {
    path: "callProtectedApi",
    component: CallProtectedApiComponent,
    data: {
      permission: {
        permittedRoles: ["Admin", "User"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقش‌های Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شده‌ی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active">
        <a [routerLink]="['/callProtectedApi']">‍‍Call Protected Api</a>
</li>


نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم

در ادامه می‌خواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شده‌ی سمت سرور را بازگشت دهند. دکمه‌ی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمه‌ی دوم توسط کاربری با نقش‌های Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-call-protected-api",
  templateUrl: "./call-protected-api.component.html",
  styleUrls: ["./call-protected-api.component.css"]
})
export class CallProtectedApiComponent implements OnInit {

  isAdmin = false;
  isUser = false;
  result: any;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }

  callMyProtectedAdminApiController() {
  }

  callMyProtectedApiController() {
  }
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفته‌اند. مقدار دهی آن‌ها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام می‌شود. اکنون که این دو خاصیت مقدار دهی شده‌اند، می‌توان از آن‌ها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمت‌های مختلف صفحه استفاده کرد:
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()">
  Call Protected Admin API [Authorize(Roles = "Admin")]
</button>

<button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()">
  Call Protected API ([Authorize])
</button>

<div *ngIf="result">
  <pre>{{result | json}}</pre>
</div>


دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال می‌کند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل می‌توان در AuthService تولید نمود:
  getBearerAuthHeader(): HttpHeaders {
    return new HttpHeaders({
      "Content-Type": "application/json",
      "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}`
    });
  }

روش دوم انجام اینکار که مرسوم‌تر است، اضافه کردن خودکار این هدر به تمام درخواست‌های ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { AuthService, AuthTokenType } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام می‌شود:
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./services/auth.interceptor";

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class CoreModule {}
در اینجا نحوه‌ی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده می‌کنید.

در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعه‌دهنده‌های مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازنده‌ی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شده‌است که این سرویس نیز دارای HttpClient تزریق شده‌ی در سازنده‌ی آن است. به همین جهت Angular تصور می‌کند که ممکن است در اینجا یک بازگشت بی‌نهایت بین این interceptor و سرویس Auth رخ‌دهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار می‌کنند، در اینجا فراخوانی نمی‌کنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را می‌توان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازنده‌ی کلاس AuthInterceptor تزریق می‌کنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازنده‌ی کلاس):
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}


تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

اکنون پس از افزودن AuthInterceptor، می‌توان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویس‌های Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازنده‌ی CallProtectedApiComponent تزریق می‌کنیم:
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
  ) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
  callMyProtectedAdminApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

  callMyProtectedApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:



مدیریت خودکار خطاهای عدم دسترسی ارسال شده‌ی از سمت سرور

ممکن است کاربری درخواستی را به منبع محافظت شده‌ای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده می‌توان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحه‌ی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
    return next.handle(request)
      .catch((error: any, caught: Observable<HttpEvent<any>>) => {
        if (error.status === 401 || error.status === 403) {
          this.router.navigate(["/accessDenied"]);
        }
        return Observable.throw(error);
      });
در اینجا ابتدا نیاز است سرویس Router، به سازنده‌ی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیت‌های 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(
    private injector: Injector,
    private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
      return next.handle(request)
        .catch((error: any, caught: Observable<HttpEvent<any>>) => {
          if (error.status === 401 || error.status === 403) {
            this.router.navigate(["/accessDenied"]);
          }
          return Observable.throw(error);
        });
    } else {
      // login page
      return next.handle(request);
    }
  }
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه می‌کنیم:
using ASPNETCore2JwtAuthentication.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace ASPNETCore2JwtAuthentication.WebApp.Controllers
{
    [Route("api/[controller]")]
    [EnableCors("CorsPolicy")]
    [Authorize(Policy = CustomRoles.Editor)]
    public class MyProtectedEditorsApiController : Controller
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]",
                Username = this.User.Identity.Name
            });
        }
    }
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
  callMyProtectedEditorsApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }
چون این نقش جدید به کاربر جاری انتساب داده نشده‌است (جزو اطلاعات سمت سرور او نیست)، اگر آن‌را توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت می‌شود:



نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور

اگر برنامه‌ی سمت سرور ما که توکن‌ها را اعتبارسنجی می‌کند، ری‌استارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکن‌های فعلی او برگشت خواهند خورد و از مرحله‌ی تعیین اعتبار رد نمی‌شوند. در این حالت کاربر خطای 401 را دریافت می‌کند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن» را فراموش نکنید.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود. 
بازخوردهای پروژه‌ها
Fill and FillByObject
متود زیر برای تولید عبارت مبتنی بر الگو زمانی که الگو پیچیده‌تر از این است که بشود با string.Format به راحتی پیاده سازی شود ولی چندان هم پیچیده نیست که ابزارهای حرفه ای‌تری مانند T4 یا Razor به کار بیایند، میتواند مورد استفاده قرار گیرد:
        /// <summary>
        /// Insert values from an object into a string pattern. To specify the object's property you have to use the '{propertyName}'.
        /// </summary>
        /// <remarks>
        /// This method is a replacement to String.Format, but it has two differences:
        /// <list type="simple">
        /// <item>It reverese the order you call the functionality, instead of writing String.Format(pattern, args) you write pattern.FillByObject(args). This makes the code look cleaner.</item>
        /// <item>You supply the pattern with an object and specify insertion points by property names.</item>
        /// </list>
        /// <example>
        /// <![CDATA[
        /// "First name:{firstName}, Sur name:{Surname}".FillByObject(new {firstName = "Sam", lastName="Naseri"});
        /// ]]>
        /// </example>
        /// </remarks>
        /// <seealso cref="Fill"/>
        /// <typeparam name="T">Type of bindingValue.</typeparam>
        /// <param name="bindingPattern">The pattern to fill.</param>
        /// <param name="bindingValue">The object providing values to fill in the pattern.</param>
        /// <returns>The pattern filled with values.</returns>
        public static string FillByObject<T>(this string bindingPattern, T bindingValue)
        {
            var properties = GetProperties(typeof(T)).ToList();
            var values = properties.Select(property => property.GetValue(bindingValue, new object[] { })).ToList();
            var result = bindingPattern;
            for (int index = 0; index < properties.Count; index++)
            {
                var property = properties[index];
                var propPattern = "{" + property.Name + "}";
                var old = result;
                result = result.Replace(propPattern, values[index] != null ? values[index].ToString() : "");
            }
            return result;
        }

پیاده سازی فوق یک پیاده سازی بسیار خام و بسیار کند است و جای بهبود زیادی دارد. من فقط برای سناریوهای ساده که کارایی مطرح نیست استفاده از متود فوق را توصیه میکنم.
متودهای جانبی مورد نیاز:
private static IEnumerable<PropertyInfo> GetProperties(Type t)
{
    return t.GetProperties(BindingFlags.Public | BindingFlags.Instance);
}

همچنین متود زیر هم میتواند نوشتن string.Format را یک خرده ساده‌تر کند:
/// <summary>
/// A simple replacement for String.Format which only makes the codes look nicer.
/// </summary>
/// <param name="pattern">The source string that you want to replace insertion points on it.</param>
/// <param name="args">Values to be replaced in the pattern.</param>
/// <returns></returns>
public static string Fill(this string pattern, params object[] args)
{
    return string.Format(pattern, args);
}

اشتراک‌ها
کتابخانه dragula

Drag and drop so simple it hurts  Demo

Framework support includes vanilla JavaScript, Angular, and React.

Features

  • Super easy to set up
  • No bloated dependencies
  • Figures out sort order on its own
  • A shadow where the item would be dropped offers visual feedback
  • Touch events!
  • Seamlessly handles clicks without any configuration 
کتابخانه dragula
مطالب
شروع به کار با DNTFrameworkCore - قسمت 6 - پیاده‌سازی عملیات CRUD موجودیت‌ها با استفاده از ASP.NET Core MVC
به عنوان آخرین قسمت مرتبط با تفکر مبتنی‌بر CRUD‏ ‎(‎CRUD-based thinking)‎، روش پیاده‌سازی عملیات CRUD موجودیت‌ها را با استفاده از ASP.NET Core MVC و افزونه jquery-unobtrusive-ajax بررسی خواهیم کرد. 


برای شروع لازم است بسته نیوگت زیر را نصب کنید: 
PM> Install-Package DNTFrameworkCore.Web

قراردادها، مفاهیم و نکات اولیه 
  1. Refactor کردن فرم‌های ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار می‌کند. برای موجودیت‌های ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده می‌شود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل می‌شود، استفاده خواهیم کرد.
  2. یک فرم، در قالب یک پارشال‌ویو، به صورت Ajaxای با استفاده از افزونه  jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
  3. یک فرم براساس طراحی خود می‌تواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان می‌باشد اضافه شود.
  4. وجود ویو Index به همراه پارشال‌ویو ‎_List برای نمایش لیستی و یک پارشال‌ویو برای عملیات ثبت و ویرایش الزامی ‌می‌باشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD»  مطرح شد، استفاده نمی‌کنید و نیاز دارید تا اطلاعات صفحه‌بندی شده، مرتب شده و فیلتر شده‌ای را در قالب JSON دریافت کنید، از اکشن‌متد ReadPagedList کنترلر پایه استفاده کنید.

مثال اول: پیاده‌سازی BlogsController و طراحی فرم ثبت و ویرایش
کار با ارث‌بری از کنترلر پایه طراحی شده در زیرساخت به شکل زیر آغاز می‌شود:
public class BlogsController : CrudController<IBlogService, int, BlogModel>
{
    public BlogsController(IBlogService service) : base(service)
    {
    }

    protected override string CreatePermissionName => PermissionNames.Blogs_Create;
    protected override string EditPermissionName => PermissionNames.Blogs_Edit;
    protected override string ViewPermissionName => PermissionNames.Blogs_View;
    protected override string DeletePermissionName => PermissionNames.Blogs_Delete;
    protected override string ViewName => "_BlogModal";
}
در اینجا مشخص کردن ViewName متناظر با موجودیت جاری، الزامی می‌باشد. همانطور که عنوان شد، یک پارشال‌ویو برای عملیات ثبت و ویرایش کفایت می‌کند و طراحی پشت این کنترلر پایه نیز بر این اساس می‌باشد. گام بعدی، طراحی پارشال‌ویو مذکور می‌باشد. 

<div>
    <h4 asp-if="Model.IsNew()">Create New Blog</h4>
    <h4 asp-if="!Model.IsNew()">Edit Blog</h4>
    <button type="button" data-dismiss="modal">&times;</button>
</div>
در اینجا با استفاده از تگ‌هلپر asp-if موجود در زیرساخت و متد IsNew متناظر با مدل جاری، عنوان نمایشی مودال را مشخص کرده‌ایم. 
 ‌


<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm">
    <div>
        <input type="hidden" name="save-continue" value="true"/>
        <input asp-for="RowVersion" type="hidden"/>
        <input asp-for="Id" type="hidden"/>
        <div>
            <div>
                <label asp-for="Title"></label>
                <input asp-for="Title" autocomplete="off"/>
                <span asp-validation-for="Title"></span>
            </div>
        </div>
        <div>
            <div>
                <label asp-for="Url"></label>
                <input asp-for="Url" type="url"/>
                <span asp-validation-for="Url"></span>
            </div>
        </div>
    </div>

...

</form>
با استفاده از متد IsNew مدل جاری، اکشن متد متناظر با درخواست POST مشخص شده و سپس از طریق تگ‌هلپر asp-modal-form، فرآیند افزودن ویژگی‌های data-ajax به فرم را خودکار کرده‌ایم. سپس با استفاده از یک Hidden Input با نام save-continue و مقدار true به فرم، عملیات ذخیره کردن بدون بسته شدن فرم را شبیه سازی کرده‌ایم؛ این Input می‌تواند به صورت شرطی به فرم اضافه شود (دکمه‌های ذخیره/ذخیره و بستن و ...) که در سمت سرور با توجه به مقدار ارسالی تصمیم گرفته می‌شود که دوباره فرم با اطلاعات جدید (در اینجا Id و RowVersion) به کلاینت ارسال شود یا خیر.

<div>
    <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete"
       title="Delete Blog">
        <i></i>
    </a>
    <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id">
        <i></i>
    </a>
    <a title="New Blog" asp-modal-link asp-modal-toggle="false"
       asp-controller="Blogs" asp-action="Create">
        <i></i>
    </a>
    <button type="button" data-dismiss="modal">
        <i></i>&nbsp; Cancel
    </button>
    <button type="submit">
        <i></i>&nbsp; Save Changes
    </button>
</div>
بخش فوتر این مودال در برگیرنده دکمه‌های متناظر با اکشن‌های قابل انجام بر روی موجودیت جاری می‌باشد.
در این فرم ابتدا 3 دکمه میانبر برای عملیات حذف، بازنشانی و جدید درنظر گرفته شده‌اند؛ برای حذف که از طریق تگ‌هلپر asp-modal-delete-link کار افزودن خودکار ویژگی‌های data-ajax انجام شده و در نهایت با توجه به اکشن‌متد و کنترلر مشخص شده، یک مودال، برای گرفتن تأیید از کاربر باز می‌شود و در زمان پذیرش درخواست، حذف مدل جاری به سرور ارسال خواهد شد. با استفاده از تگ‌هلپر asp-permission نیز دسترسی مورد نیاز برای مشاهده دکمه مورد نظر را مشخص کرده‌ایم. دکمه‌های بازنشانی و جدید، در اینجا از طریق تگ‌هلپر asp-modal-link تبدیل به یک لینک Ajaxای شده است که کار بارگذاری یک پارشال‌ویو را انجام می‌دهند؛ همچنین با استفاده از ویژگی asp-modal-toggle متناظر با تگ‌هلپر مذکور با مقدار false، مشخص کرده‎ایم که صرفا محتوای مودال جاری جایگزین شود و نیاز به گشوده شدن دوباره آن نیست.


خروجی نهایی در حالت ویرایش به شکل زیر می‌باشد:

‌‌
مثال دوم: پیاده‌سازی RolesController و طراحی فرم ثبت و ویرایش
‌‌نکته قابل توجه در طراحی فرم‌های ثبت و ویرایش موجودیت‌های پیچیده‌تر مربوط است به ارسال اطلاعات اضافه‌تر از هر آنچه در مدل وجود دارد به کلاینت و همچنین دریافت اطلاعات در ساختار تودرتو یا به اصطلاح یک گراف از اشیاء. به عنوان مثال برای ثبت یک گروه کاربری نیاز است لیست دسترسی‌ها را نیز در فرم نمایش دهیم تا توسط کاربر انتخاب شود؛ در این حالت نیاز است یک مدل متناظر و متناسب را به شکل زیر طراحی کرد:
public class RoleModalViewModel : RoleModel
{
    public IReadOnlyList<LookupItem> PermissionList { get; set; }
}
در اینجا با ارث‌بری از RoleModel متناظر با موجودیت گروه کاربری، لیست دسترسی‌ها نیز در طریق خصوصیت PermissionList قابل استفاده می‌باشد. حال برای مقداردهی خصوصیت اضافه شده و به طور کلی برای ارسال اطلاعات اضافه به کلاینت در زمان بارگذاری فرم، نیاز است متد RenderView را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderView(RoleModel role)
{
    var model = _mapper.Map<RoleModalViewModel>(role);
    model.PermissionList = ReadPermissionList();

    return PartialView(ViewName, model);
}
به عنوان مثال در اینجا از AutoMapper برای وهله سازی از RoleModalViewModel و نگاشت خصوصیات RoleModel، استفاده شده است. به این ترتیب خروجی نهایی در حالت ویرایش به شکل زیر می‌باشد:



برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسی‌ها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیاده‌سازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار می‌کنم.


نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت می‌توانید متد RenderIndex را به شکل زیر بازنویسی کنید:

protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model)
{
    var indexModel = new RoleIndexViewModel
    {
        Items = model.Items,
        TotalCount = model.TotalCount,
        Permissions = ReadPermissionList()
    };

    return Request.IsAjaxRequest()
        ? (IActionResult) PartialView(indexModel)
        : View(indexModel);
}

مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:

public class RoleIndexViewModel : PagedQueryResult<RoleReadModel>
{
    public IReadOnlyList<LookupItem> Permissions { get; set; }
}


فرآیند بارگذاری یک پارشال‌ویو در مودال

به عنوان مثال برای استفاده از مودال‌های بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:

<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-body">
                Loading...
            </div>
        </div>
    </div>
</div>

سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشال‌ویو از سرور، آن را با محتوای modal-content جایگزین می‌کنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی می‌کنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:‌‌

    /*----------------------------------  asp-modal-link ---------------------------*/
    window.handleModalLinkLoaded = function (data, status, xhr) {
        prepareForm('#main-modal.modal form');
    };
    window.handleModalLinkFailed = function (xhr, status, error) {
        //....
    };
    /*----------------------------------  asp-modal-form ---------------------------*/
    window.handleModalFormBegin = function (xhr) {
        $('#main-modal a').addClass('disabled');
        $('#main-modal button').attr('disabled', 'disabled');
    };
    window.handleModalFormComplete = function (xhr, status) {
        $('#main-modal a').removeClass('disabled');
        $('#main-modal button').removeAttr('disabled');
    };
    window.handleModalFormSucceeded = function (data, status, xhr) {
        if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') {
            prepareForm('#main-modal.modal form');
        } else {
            hideMainModal();
        }
    };
    window.handleModalFormFailed = function (xhr, status, error, formId) {
        if (xhr.status === 400) {
            handleBadRequest(xhr, formId);
        }
    };


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید. 
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت سوم - روش اتصال Redux به برنامه‌های React
پس از بررسی ساختار کتابخانه‌ی Redux به صورت مستقل و متکی به خود، اکنون در این قسمت، نحوه‌ی اتصال آن‌را به برنامه‌های React بررسی می‌کنیم.


نصب پیشنیازها

می‌توان همانند قسمت قبل، تمام کارها را با کتابخانه‌ی redux انجام داد و یا می‌توان قسمت به روز رسانی UI آن‌را و همچنین مدیریت state را به کتابخانه‌ی ساده کننده‌ی دیگری به نام react-redux واگذار کرد. به همین جهت در ادامه‌ی همان برنامه‌ی قسمت قبل، دو کتابخانه‌ی redux و همچنین react-redux را به همراه types آن نصب می‌کنیم (نصب types، سبب ارائه‌ی intellisense بهتری در VSCode می‌شود؛ حتی اگر نخواهیم با TypeScript کار کنیم).
برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save redux react-redux
> npm install --save-dev @types/react-redux
به علاوه در ادامه توئیتر بوت استرپ 4 را نیز نصب می‌کنیم:
> npm install --save bootstrap
سپس برای افزودن فایل bootstrap.css به پروژه‌ی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.


معرفی ساختار ابتدایی برنامه

برنامه‌ای را که در این قسمت بررسی می‌کنیم، ساختار بسیار ساده‌ای را داشته و به همراه دو دکمه‌ی افزایش و کاهش مقدار یک شمارشگر است؛ به همراه دکمه‌ی برای به حالت اول در آوردن آن. هدف اصلی دنبال شده‌ی در اینجا نیز نحوه‌ی برپایی redux و همچنین react-redux و اتصال آن‌ها به برنامه‌ی React جاری است:


به همین جهت ابتدا کامپوننت جدید src\components\counter.jsx را به نحو زیر تشکیل می‌دهیم تا markup ابتدایی فوق را به همراه سه دکمه و یک span، برای نمایش مقدار شمارشگر، رندر کند:
import React, { Component } from "react";

class Counter extends Component {
  render() {
    return (
      <section className="card mt-5">
        <div className="card-body text-center">
          <span className="badge m-2 badge-primary">0</span>
        </div>
        <div className="card-footer">
          <div className="d-flex justify-content-center align-items-center">
            <button className="btn btn-secondary btn-sm">+</button>
            <button className="btn btn-secondary btn-sm m-2">-</button>
            <button className="btn btn-danger btn-sm">Reset</button>
          </div>
        </div>
      </section>
    );
  }
}

export default Counter;
سپس المان آن‌را جهت نمایش در برنامه، به فایل src\App.js اضافه می‌کنیم:
import "./App.css";

import React from "react";

import Counter from "./components/counter";

function App() {
  return (
    <main className="container">
      <Counter />
    </main>
  );
}

export default App;


پوشه بندی مخصوص برنامه‌های مبتنی بر Redux


هدف ما در ادامه ایجاد یک store مخصوص redux است و سپس اتصال آن به کامپوننت شمارشگر برنامه. به همین جهت نیاز به 4 پوشه‌ی جدید، برای مدیریت بهتر برنامه خواهیم داشت:
- پوشه constants: برای اینکه نام رشته‌ای نوع اکشن‌های مختلف را بتوانیم در قسمت‌های مختلف برنامه استفاده کنیم، بهتر است فایل جدید src\actions\index.js را ایجاد کرده و این ثوابت را داخل آن export کنیم.
- پوشه‌ی actions: در فایل جدید src\actions\index.js، تمام متدهای ایجاد کننده‌ی شیء خاص action، که در قسمت قبل در مورد آن بحث شد، قرار می‌گیرند. نمونه‌ی آن، متد createAddAction قسمت قبل است.
- پوشه‌ی reducers: تمام توابع reducer برنامه را در فایل‌های مجزایی در پوشه‌ی reducers قرار می‌دهیم. سپس در فایل src\reducers\index.js با استفاده از متد combineReducer آن‌ها را یکی کرده و به متد createStore ارسال می‌کنیم.
- پوشه‌ی containers: این پوشه جائی است که کار فراخوانی متد connect کتابخانه‌ی react-redux به ازای هر کامپوننت استفاده کننده‌ی از redux store، صورت می‌گیرد.

این موارد را با جزئیات بیشتری در ادامه بررسی می‌کنیم.



ایجاد نام نوع اکشن متناظر با دکمه‌ی افزودن مقدار

می‌خواهیم با کلیک بر روی دکمه‌ی +، مقدار شمارشگر افزایش یابد. به همین جهت نیاز به یک نام وجود دارد تا در تابع Reducer متناظر و قسمت‌های دیگر برنامه، بتوان بر اساس آن، این اکشن خاص را شناسایی کرد و سپس عکس العمل نشان داد. به همین جهت فایل جدید src\constants\ActionTypes.js را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
export const Increment = "Increment";
البته هرچند مرسوم است نام و مقدار این نوع ثوابت را تشکیل شده‌ی از حروف بزرگ، معرفی کنند ولی این موضوع اختیاری است.


ایجاد متد Action Creator

در قسمت قبل مشاهده کردیم که شیء ارسالی به یک reducer از طریق dispatch یک action خاص، دارای فرمت ویژه‌ی زیر است:
{
    type: "ADD",
    payload: {
      amount // = amount: amount
    },
    meta: {}
}
به همین جهت برای نظم بخشیدن به تعریف این نوع اشیاء و یک‌دست سازی آن‌ها، فایل جدید src\actions\index.js را ایجاد کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
import * as types from "../constants/ActionTypes";

export const incrementValue = () => ({ type: types.Increment });
همانطور که ملاحظه می‌کنید در این متد، فعلا فقط نام رشته‌ای نوع این اکشن، بیشتر مدنظر است تا بر اساس action.type رسیده در reducer متناظر با آن، عملی رخ دهد. بنابراین فقط قسمت type آن‌را مقدار دهی کرده‌ایم. مقدار ثابت رشته‌ای types.Increment نیز از فایل مجزای src\constants\ActionTypes.js که پیشتر تعریف کردیم، تامین شده‌است.


ایجاد تابع reducer مخصوص افزودن مقدار

ابتدا فایل جدید src\reducers\counter.js را با محتوای زیر ایجاد می‌کنیم:
import * as types from "../constants/ActionTypes";

const initialState = {
  count: 0
};

export default function counterReducer(state = initialState, action) {
  if (action.type === types.Increment) {
    return {
      count: state.count + 1
    };
  }
  return state;
}
- اگر دقت کرده باشید، کامپوننت شمارشگر این قسمت، دارای state نیست و همچنین نمی‌خواهیم هم که دارای state باشد؛ چون قرار است توسط redux مدیریت شود. به همین جهت state اولیه را به صورت initialState که محتوای یک شیء با خاصیت count با مقدار اولیه‌ی صفر است، خارج از کلاس کامپوننت، ایجاد کرده‌ایم.
- سپس می‌خواهیم رویداد کلیک بر روی دکمه + را مدیریت کنیم. به همین جهت نیاز به یک اکشن جدید به نام Increment داریم که توسط مقدار ثابت رشته‌ای types.Increment، از فایل مجزای src\constants\ActionTypes.js، تامین می‌شود.
- پس از مشخص کردن نوع action ای که قرار است مدیریت شود و همچنین ایجاد متدی برای تولید شیء حاوی اطلاعات آن که در فایل src\actions\index.js قرار دارد، اکنون می‌توان متد reducer را که state و action را دریافت می‌کند و سپس state جدیدی را بر اساس action.type دریافتی و در صورت نیاز بازگشت می‌دهد، ایجاد کرد. این متد بررسی می‌کند که آیا action.type رسیده همان ثابت Increment است؟ اگر بله، بجای تغییر مستقیم state.count، یک شیء جدید را بازگشت می‌دهد. البته روش صحیح‌تر اینکار را در قسمت اول این سری با معرفی روش‌هایی برای کپی اشیاء و آرایه‌ها، بررسی کردیم. در اینجا جهت سادگی بیشتر، یک شیء کاملا جدید را دستی ایجاد می‌کنیم. در آخر اگر action.type رسیده قابل پردازش نبود، همان state ابتدایی دریافتی را بازگشت می‌دهیم تا در صورت وجود چندین reducer تعریف شده‌ی در سیستم، زنجیره‌ی آن‌ها قابل پردازش باشد. این مورد را در قسمت قبل، ذیل عنوان «بررسی تابع combineReducers با یک مثال» بیشتر بررسی کردیم.

پس از ایجاد reducer اختصاصی عمل افزودن مقدار شمارشگر، فایل جدید src\reducers\index.js را نیز با محتوای زیر ایجاد می‌کنیم:
import { combineReducers } from "redux";

import counterReducer from "./counter";

const rootReducer = combineReducers({
  counterReducer
});

export default rootReducer;
کار این فایل، مدیریت مرکزی تمام reducerهای سفارشی تعریف شده‌ی در برنامه‌است. لیست آن‌ها را به متد combineReducers ارسال کرده و در نهایت یک rootReducer ترکیب شده‌ی از تمام آن‌ها را دریافت می‌کنیم.


ایجاد store مخصوص Redux

تا اینجا رسیدیم به یک rootReducer متشکل از تمام reducerهای سفارشی برنامه. اکنون بر اساس آن در فایل src\index.js، یک store جدید را ایجاد می‌کنیم:
import { createStore } from "redux";
import reducer from "./reducers";

//...

const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

//...
نکته 1: چون شیء rootReducer در فایل src\reducers\index.js واقع شده‌است، دیگر در حین import، نیازی به ذکر نام فایل index آن نیست.
نکته 2: در اینجا روش فعالسازی افزونه‌ی redux-devtools را نیز ملاحظه می‌کنید. ابتدا بررسی می‌شود که آیا متد ویژه‌ی فراخوانی این افزونه وجود دارد یا خیر؟ اگر بله، فراخوانی می‌شود. بدون این پارامتر دوم، افزونه‌ی redex dev tools، هیچ خروجی را نمایش نخواهد داد.


اتصال React به Redux

کتابخانه‌ی react-redux تنها به همراه دو شیء مهم connect و Provider است. شیء Provider آن شبیه به Context API خود React است و هدف آن، ارسال ارجاعی از store ایجاد شده، به برنامه‌ی React است. پس از ایجاد store در فایل src\index.js، اکنون نوبت به اتصال آن به برنامه‌ی React ای جاری است. به همین جهت در بالاترین سطح برنامه، ابتدا شیء کامپوننت App را با شیء Provider محصور می‌کنیم:
import { Provider } from "react-redux";
import { createStore } from "redux";
import reducer from "./reducers";

// ...
const store = createStore(
  reducer,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
کامپوننت Provider، از طریق props خود نیاز به دریافت store تعریف شده را دارد. به این ترتیب هر کامپوننتی که در درخت کامپوننت‌های App قرار می‌گیرد، می‌تواند با redux store کار کند.


تامین state کامپوننت شمارشگر از طریق props

همانطور که عنوان شد، کامپوننت Counter به همراه state نیست و ما قصد نداریم در آن از state خود React استفاده کنیم؛ البته فلسفه‌ی آن‌را در قسمت اول این سری بررسی کردیم و همچنین اگر کامپوننتی نیاز به اشتراک گذاری اطلاعات خودش را با لایه‌های زیرین یا بالاتر از خود ندارد، شاید اصلا نیازی به Redux نداشته باشد و همان state استاندارد React برای آن کافی است. بنابراین می‌توان برنامه‌ای را داشت که ترکیبی از state استاندارد React، در کامپوننت‌های متکی به خود و Redux، در کامپوننت‌هایی که باید اطلاعاتی را با هم به اشتراک بگذارند، باشد. برای مثال، کامپوننت مثال جاری، واقعا نیازی را به Redux، برای مدیریت حالت خود، ندارد؛ هدف ما در اینجا بررسی نحوه‌ی برقراری ارتباطات یک سیستم مبتنی بر Redux، در برنامه‌های React است.
بنابراین در اینجا و کامپوننتی که قرار است از Redux برای مدیریت حالت خود استفاده کند، هر اطلاعاتی که به آن از طریق react-redux store وارد می‌شود، از طریق props به آن ارسال خواهد شد. برای مثال در اینجا مقدار count، از طریق props خوانده می‌شود و همچنین امکان ارسال action ای خاص به متد reducer تعریف شده نیز باید تعریف شود. بنابراین در ادامه نیاز داریم تا یک کامپوننت React را به redux store متصل کنیم. برای این منظور فایل جدید src\containers\Counter.js را با محتوای زیر ایجاد می‌کنیم:
import { connect } from "react-redux";

import { incrementValue } from "../actions";
import Counter from "../components/counter";

const mapStateToProps = state => {
  return state;
};

const mapDispatchToProps = dispatch => {
  return {
    increment() {
      dispatch(incrementValue());
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
ابتدا متد connect را از react-redux دریافت می‌کنیم. connect خود متدی است که منتظر یک کامپوننت React است؛ مانند Counter. همچنین به عنوان پارامتر، توابعی را دریافت می‌کند که اطلاعات redux store را به کامپوننت، نگاشت می‌کنند؛ مانند props و actions. در اینجا دو تابع نگاشت state به props و همچنین dispatch به props را ملاحظه می‌کنید (توابع mapStateToProps و mapDispatchToProps)؛ هرچند الزامی نیست، ولی بهتر است از همین روش نامگذاری استفاده شود.

زمانیکه در مورد store در redux صحبت می‌شود، داخل آن یک شیء بزرگ state قرار گرفته‌است که حاوی کل state برنامه‌است. اما شاید هر کامپوننت به تمام آن نیازی نداشته باشد. برای مثال شاید کامپوننت شمارشگر، اهمیتی به اطلاعات خطاهای سیستم و یا کاربر وارد شده‌ی به سیستم که در شیء کلی state موجود در store وجود دارند، ندهد. به همین جهت متد mapStateToProps، کل state برنامه را دریافت کرده و به ما اجازه می‌دهد تا تنها اطلاعاتی را که از آن نیاز داریم، به صورت props دریافت کنیم. به این ترتیب از رندر مجدد این کامپوننت نیز جلوگیری خواهد شد؛ چون این کامپوننت دیگر وابسته‌ی به تغییرات سایر اجزای کل state برنامه، نخواهد بود و اگر آن‌ها تغییر کردند، این کامپوننت رندر مجدد نخواهد شد.
بنابراین می‌توان متد mapStateToProps را به صورت کلی زیر نیز تعریف کرد:
const mapStateToProps = (state) => { return state };
هرچند این روش در مثال ما بدون مشکل کار می‌کند، اما چون کل state را دریافت می‌کند، مشکل رندر مجدد کامپوننت را به ازای هر تغییری در state کلی برنامه به همراه خواهد داشت.

یک نکته: اگر کامپوننتی نیاز به تامین state خود را از طریق props نداشت و فقط کارش صدور رخ‌دادها است، می‌توان پارامتر اول متد connect را نال وارد کرد.

پارامتر dispatch متد mapDispatchToProps، به متد store.dispatch اشاره می‌کند. بنابراین توسط آن امکان ارسال actions را میسر کرده و می‌توان state را توسط reducerهای تعریف شده، تغییر داد که در نتیجه‌ی آن props جدیدی به کامپوننت منتقل می‌شوند. این تابع نیز یک شیء را باز می‌گرداند. این شیء را فعلا با یک متد دلخواه مقدار دهی می‌کنیم که توسط پارامتر dispatch رسیده‌ی به آن، متد action creator تعریف شده‌ی در فایل src\actions\index.js را به نام incrementValue، فراخوانی می‌کند؛ دقیقا عملی شبیه به فراخوانی store.dispatch(createAddAction(2)) در قسمت قبل که از آن برای ارسال یک اکشن، به reducer متناظری استفاده شد.

یک نکته: اگر کامپوننتی کار صدور رخ‌دادها را انجام نمی‌دهد، می‌توان پارامتر دوم متد connect را بطور کامل حذف کرد و قید نکرد.


استفاده از کامپوننت جدید خروجی متد connect، جهت تامین props کامپوننت شمارشگر

در انتهای فایل src\components\counter.jsx، چنین سطری درج شده‌است:
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
این شیء حاصل، به خودی خود، سبب بروز تغییری در کامپوننت شمارشگر نمی‌شود. بلکه یک کامپوننت دربرگیرنده‌ی کامپوننت Counter را ایجاد می‌کند (به همین جهت آن‌را در پوشه‌ی containers یا دربرگیرنده‌ها قرار دادیم). بنابراین برای استفاده‌ی از آن، به کامپوننت src\App.js مراجعه کرده و جائیکه المان کامپوننت Counter قبلی درج شده، آن‌را به صورت زیر تغییر می‌دهیم:
import "./App.css";

import React from "react";

import CounterContainer from "./containers/Counter";

function App() {
  return (
    <main className="container">
      <CounterContainer />
    </main>
  );
}

export default App;
ابتدا کامپوننت جدید CounterContainer را که تبادل اطلاعات بین کامپوننت Counter اصلی و state و action مخزن redux را برقرار می‌کند، import کرده و سپس المان جدید آن‌را جایگزین المان کامپوننت شمارشگر اصلی می‌کنیم.

اکنون کامپوننت شمارشگر src\components\counter.jsx، دو شیء را از طریق props دریافت می‌کند؛ یکی کل state است که خاصیت count داخل آن قرار دارد و از طریق mapStateToProps تامین می‌شود. دیگری متد increment ای است که در متد mapDispatchToProps تعریف کردیم و کار صدور رخ‌دادی را به reducer متناظر، انجام می‌دهد. به همین جهت تغییرات ذیل را در کامپوننت Counter اعمال می‌کنیم:
import React, { Component } from "react";

class Counter extends Component {
  render() {
    console.log("props", this.props);
    const {
      counterReducer: { count },
      increment
    } = this.props;
    return (
      <section className="card mt-5">
        <div className="card-body text-center">
          <span className="badge m-2 badge-primary">{count}</span>
        </div>
        <div className="card-footer">
          <div className="d-flex justify-content-center align-items-center">
            <button className="btn btn-secondary btn-sm" onClick={increment}>
              +
            </button>
            <button className="btn btn-secondary btn-sm m-2">-</button>
            <button className="btn btn-danger btn-sm">Reset</button>
          </div>
        </div>
      </section>
    );
  }
}

export default Counter;
لاگ اولین بار دریافت this.pros این کامپوننت که اکنون توسط دربرگیرنده‌ی آن ارائه می‌شود، به صورت زیر است:


به همین جهت، خاصیت تو در توی this.props.counterReducer.count و همچنین اشاره‌گر به متد increment، توسط Object Destructuring به صورت زیر از this.props دریافتی، تجزیه شده‌اند:
    const {
      counterReducer: { count },
      increment
    } = this.props;
سپس مقدار count، توسط span نمایش داده و همچنین دکمه +  را به صورت onClick={increment} تکمیل کرده‌ایم تا با کلیک بر روی آن، متد increment که در حقیقت معادل فراخوانی store.dispatch(incrementValue()) است، اجرا شود. حاصل آن، افزایش مقدار شمارشگر است:


جزئیات کار با Redux store را نیز می‌توان در افزونه‌ی redux dev tools مشاهده کرد:


این افزونه در نوار ابزار پایین آن، امکان export کل state و سپس import و بازیابی آن‌را نیز به همراه دارد.


دریافت props از طریق کامپوننت دربرگیرنده و ارسال آن به کامپوننت اصلی

فرض کنید نیاز باشد تا اطلاعاتی را به صورت متداول React از طریق props، به کامپوننت دربرگیرنده‌ی کامپوننت شمارشگر ارسال کرد:
function App() {
  const prop1 = 123
  return (
    <main className="container">
      <CounterContainer prop1={prop1} />
    </main>
  );
}
برای دسترسی به آن، پارامتر دومی به متد mapStateToProps به نام ownProps اضافه می‌شود که حاوی props ارسالی به کامپوننت container است:
const mapStateToProps = (state, ownProps) => {
  console.log("mapStateToProps", { state, ownProps });
  return state;
};
در این حالت اگر نیاز به انتقال آن به کامپوننت اصلی بود، می‌توان شیء بازگشت داده شده‌ی از mapStateToProps را به همراه یک سری خواص سفارشی دریافتی از ownProps، تعریف کرد.


پیاده سازی دکمه‌ی کاهش مقدار شمارشگر

پس از آشنایی با روش کلی برقراری اتصالات سیستم react-redux، پیاده سازی دکمه‌ی کاهش مقدار شمارشگر بسیار ساده‌است و شامل مراحل زیر است:
1)  ایجاد نام نوع اکشن متناظر با دکمه‌ی کاهش مقدار
به فایل src\constants\ActionTypes.js، نوع جدید کاهشی را اضافه می‌کنیم:
export const Decrement = "Decrement";
2) ایجاد متد Action Creator
در فایل src\actions\index.js، متد ایجاد کننده‌ی شیء اکشن ارسالی به reducer متناظری را تعریف می‌کنیم تا بتوان بر اساس نوع آن در reducer کاهشی، منطق کاهش را پیاده سازی کرد:
export const decrementValue = () => ({ type: types.Decrement });
3) ایجاد تابع reducer مخصوص کاهش مقدار
اکنون در فایل src\reducers\counter.js، بر اساس نوع شیء رسیده، تصمیم به کاهش یا افزایش مقدار موجود در state گرفته می‌شود:
export default function counterReducer(state = initialState, action) {

  // ...

  if (action.type === types.Decrement) {
    return {
      count: state.count - 1
    };
  }

  return state;
}
4) تامین state کامپوننت شمارشگر از طریق props
در ادامه نیاز است بتوان اکشن کاهش را به این reducer ارسال کرد. به همین جهت به کامپوننت دربرگیرنده‌ی کامپوننت شمارشگر در فایل src\containers\Counter.js مراجعه کرده و به شیء خروجی متد mapDispatchToProps، متد کاهش را اضافه می‌کنیم:
import { decrementValue, incrementValue } from "../actions";
// ...

const mapDispatchToProps = dispatch => {
  return {
    // ...
    decrement() {
      dispatch(decrementValue());
    }
  };
};
5) استفاده از نتایج دریافتی از props
در آخر به فایل src\components\counter.jsx مراجعه کرده و اشاره‌گر به متد decrement را از طریق this.props دریافت می‌کنیم:
const {
      // ...
      decrement
    } = this.props;
 سپس آن‌را به onClick دکمه‌ی کاهش، انتساب خواهیم داد:
<button
  className="btn btn-secondary btn-sm m-2"
  onClick={decrement}
>
  -
</button>

به عنوان تمرین، پیاده سازی دکمه‌ی Reset را نیز انجام دهید که جزئیات آن بسیار شبیه به دو مثال قبلی افزودن و کاهش مقدار شمارشگر است.


بهبود کیفیت کدهای کامپوننت دربرگیرنده‌ی کامپوننت Counter

متد mapDispatchToProps فایل src\containers\Counter.js اکنون چنین شکلی را پیدا کرده‌است:
const mapDispatchToProps = dispatch => {
  return {
    increment() {
      dispatch(incrementValue());
    },
    decrement() {
      dispatch(decrementValue());
    }
  };
};
می‌توان با استفاده از تابع bindActionCreators که در قسمت قبل در مورد آن بحث شد، تعریف آن‌را به صورت زیر خلاصه کرد:
import { bindActionCreators } from "redux";

// ...

const mapDispatchToProps = dispatch => {
  return bindActionCreators(
    {
      incrementValue,
      decrementValue
    },
    dispatch
  );
};
با استفاده از تابع bindActionCreators کتابخانه‌ی redux، می‌توان تمام action creators واقع در فایل src\actions\index.js را به صورت یک شیء به آن ارسال کرد و پارامتر دوم آن‌را نیز به store.dispatch یا در اینجا به همان dispatch دریافتی توسط پارامتر dispatch متد mapDispatchToProps، تنظیم کرد. البته در این حالت props دریافتی در کامپوننت شمارشگر به صورت زیر تغییر می‌کنند:


به همین جهت نیاز است در متد رندر کامپوننت src\components\counter.jsx، نام‌هایی را که به متدهای action creator اشاره می‌کنند، به صورت زیر تغییر داد:
const {
      counterReducer: { count },
      incrementValue,
      decrementValue
    } = this.props;
و همچنین نام‌های منتسب به onClickها را نیز بر این اساس، اصلاح کرد.

روش دوم: در نگارش‌های اخیر react-redux می‌توان متد mapDispatchToProps را به صورت زیر نیز خلاصه و تعریف کرد که بسیار ساده‌تر است:
const mapDispatchToProps = {
  incrementValue,
  decrementValue
};
البته در این حالت نیز مابقی آن که شامل تغییر نام‌ها می‌شود، یکسان است.

همچنین بجای بازگشت کل state در متد mapStateToProps، می‌توان تنها خواص مدنظر را بازگشت داد:
const mapStateToProps = state => {
  //return state;
  return {
    count: state.counterReducer.count
  };
};
در این حالت props ارسالی به کامپوننت یک چنین شکلی را پیدا می‌کنند:


بنابراین باید در متد رندر کامپوننت شمارشگر، خاصیت count را به صورت معمولی دریافت کرد:
const {
      //counterReducer: { count },
      count,
      incrementValue,
      decrementValue
    } = this.props;

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: state-management-redux-mobx-part03.zip
مطالب دوره‌ها
افزودن مشخص کننده فیلد اجباری در ASP.NET MVC به صورت خودکار توسط jQuery
در ASP.NET MVC زمانیکه خاصیتی با ویژگی Required مزین می‌شود، تا زمان اعتبار سنجی فرم، هیچ نشانی را از خود بروز نمی‌دهد. برای مثال علاقمندیم مانند شکل زیر، یک ستاره پس از فیلدهای اجباری ظاهر گردد:


انجام اینکار نیز با دو سطر کد نویسی توسط jQuery قابل انجام است.
در ادامه خروجی رندر شده فیلدی را که پرکردن آن الزامی است مشاهده می‌کنید:
        <div class="editor-label">
            <label for="Title">عنوان</label>
        </div>
        <div class="editor-field">
            <input class="text-box single-line" data-val="true" data-val-required="*" id="Title" name="Title" type="text" value="" />
            <span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
        </div>
تنها کاری که باید صورت گیرد، یافتن مواردی است که حاوی data-val-required هستند و سپس افزودن یک ستاره پس از آن‌ها:
        $(function() {    
            $('[data-val-required]').after('<span class="required-indicator"> (*)</span>');
        });
برای اعمال رنگ به آن نیز می‌توان کمی css سایت را ویرایش کرد:
.required-indicator
{
color: red;
font-size: 1.2em;
font-weight: bold;
}
نظرات مطالب
React 16x - قسمت 5 - کامپوننت‌ها - بخش 2 - نمایش لیست‌ها و مدیریت رویدادها و حالات
اگر نخواهید از arrow function‌ها برای انتقال پارامترها استفاده کنید، می‌شود این مقدار را به value نسبت داد:
<button onClick={this.handleRemove} value={id}>Remove</button>
و بعد آن‌را در متد handleRemove از طریق شیء رخداد رسیده خواند:
handleRemove(event) {
 const id = event.target.value;
 // ...
}

و یا یک روش دیگر آن استفاده از currying است که یک متد، متد دومی را بازگشت می‌دهد:
handleChange(param) { //ES-5
    return function (event) { 

    };
}

handleChange = param => event => {//ES-6

};

<input 
    type="text" 
    onChange={this.handleChange(someParam)} 
/>
در این حالت پارامتر اول را شما تنظیم می‌کنید (و کار فراخوانی این متد هنوز به پایان نرسیده) و پارامتر دوم را که شیء رخداد است، خود React ارسال می‌کند؛ یعنی فراخوانی نهایی به این صورت است: this.handleChange(someParam)(event) 
یک مثال دیگر از currying :
class App extends Component {
  oneParameter = a => e => {
    alert(a);
  };

  twoParameter = (a, b) => e => {
    alert(a + b);
  };

  render() {
    return (
      <div>
        <button onClick={this.oneParameter(32)}> one parameter </button>
        <br />
        <br />
        <button onClick={this.twoParameter(10, 54)}> two parameter</button>
      </div>
    );
  }
}
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت یازدهم - کار با فرم‌ها - قسمت دوم
در قسمت قبل، فر‌مهای template driven را بررسی کردیم. همانطور که مشاهده کردید، این نوع فرم‌ها، قابلیت‌های اعتبارسنجی پیشرفته‌ای را به همراه ندارند. برای فرم‌هایی که نیاز به اعتبارسنجی‌های سفارشی دارند، فرم‌های model driven پیشنهاد می‌شوند که در این قسمت بررسی خواهند شد.


طراحی فرم ثبت نام کاربران در سایت با روش model driven

در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجی‌های پیشرفته‌ای پیاده سازی کنیم. به همین منظور، ابتدا پوشه‌ی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
export interface IUser {
    id: number;
    name: string;
    email: string;
    password: string;
}

قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
<form>
    <div class="form-group">
        <label form="name">Username</label>
        <input id="name" type="text" class="form-control" />
    </div>
    <div class="form-group">
        <label form="email">Email</label>
        <input id="email" type="text" class="form-control" />
    </div>
    <div class="form-group">
        <label form="password">Password</label>
        <input id="password" type="password" class="form-control" />
    </div>
    <button class="btn btn-primary" type="submit">Submit</button>
</form>
اکنون می‌خواهیم این فرم را به یک فرم AngularJS 2.0 ارتقاء دهیم. بنابراین نیاز است اشیاء Control و ControlGroup را ایجاد کنیم و اینبار نمی‌خواهیم AngularJS 2.0 مانند قسمت قبلی، به صورت خودکار (و ضمنی)، این اشیاء را برای ما ایجاد کند. می‌خواهیم آن‌ها را با کدنویسی (به صورت صریح) ایجاد کنیم تا بتوانیم بر روی آن‌ها کنترل بیشتری داشته باشیم.
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
import { Component } from '@angular/core';
import { Control, ControlGroup, Validators } from '@angular/common';
 
@Component({
    selector: 'signup-form',
    templateUrl: 'app/users/signup-form.component.html'
})
export class SignupFormComponent {
    form = new ControlGroup({
        name: new Control('', Validators.required),
        email: new Control('', Validators.required),
        password: new Control('', Validators.required)
    });
 
    onSubmit(): void {
        console.log(this.form.value);
    }
}
و همچنین پیام‌های اعتبارسنجی اولیه را نیز به نحو زیر به فایل signup-form.component.html اضافه می‌کنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
    <div class="form-group">
        <label form="name">Username</label>
        <input id="name" type="text" class="form-control"
               ngControl="name"/>
        <label class="text-danger" *ngIf="!form.controls['name'].valid">
            Username is required.
        </label>
    </div>
    <div class="form-group">
        <label form="email">Email</label>
        <input id="email" type="text" class="form-control"
               ngControl="email" #email="ngForm"/>
        <label class="text-danger" *ngIf="email.touched && !email.valid">
            Email is required.
        </label>
    </div>
    <div class="form-group">
        <label form="password">Password</label>
        <input id="password" type="password" class="form-control"
               ngControl="password" #password="ngForm"/>
        <label class="text-danger" *ngIf="password.touched && !password.valid">
            Password is required.
        </label>
    </div>
    <button class="btn btn-primary" type="submit">Submit</button>
</form>
توضیحات:
تفاوت مهم این فرم و اعتبارسنجی‌هایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
form = new ControlGroup({
    name: new Control('', Validators.required),
    email: new Control('', Validators.required),
    password: new Control('', Validators.required)
});
کلا‌س‌های Control، ControlGroup و Validators در ماژول angular/common@ تعریف شده‌اند. بنابراین import متناظری نیز به ابتدای فایل اضافه شده‌است:
 import { Control, ControlGroup, Validators } from '@angular/common';

یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آن‌را در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجه‌ی جستجو، به همراه نام ماژول کلاس‌ها نیز می‌باشد.


خاصیت عمومی form که با new ControlGroup تعریف شده‌است، حاوی تعاریف صریح کنترل‌های موجود در فرم خواهد بود. در اینجا سازنده‌ی ControlGroup، یک شیء را می‌پذیرد که کلیدهای آن، همان نام کنترل‌های تعریف شده‌ی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف می‌کند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شده‌اند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیده‌اند.

اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آن‌را در قالب فرم، به ngFormModel بایند می‌کنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
به این ترتیب به AngularJS 2.0 اعلام می‌کنیم که ControlGroup و Controlهای آن‌را به صورت صریح ایجاد کرده‌ایم و بجای وهله‌‌های پیش فرض خود، از خاصیت عمومی form کلاس کامپوننت، این مقادیر را تامین کن.
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شده‌است تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شده‌است تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون می‌توان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
 <label class="text-danger" *ngIf="!form.controls['name'].valid">
ج) از این متغیر محلی جهت نمایش یا عدم نمایش پیام‌های خطای اعتبارسنجی، در ngIfهای تعریف شده، استفاده شده‌است.
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کرده‌ایم. همانطور که ملاحظه می‌کنید اینبار دیگر پارامتری را به آن ارسال نکرده‌ایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
    onSubmit(): void {
        console.log(this.form.value);
    }
this.form.value حاوی یک شیء است که تمام مقادیر پر شده‌ی فرم را به همراه دارد.

بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنتر‌ل‌های آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرم‌ها، فرم‌های model driven هم می‌گویند.


نمایش فرم افزودن کاربران توسط سیستم Routing

با نحوه‌ی تعریف مسیریابی‌ها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، می‌توان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before...
import { SignupFormComponent } from './users/signup-form.component';
 
@Component({
    //same as before…
    template: `
                //same as before…                    
                <li><a [routerLink]="['AddUser']">Add User</a></li>
               //same as before…
    `,
    //same as before…
})
@RouteConfig([
    //same as before…    
    { path: '/adduser', name: 'AddUser', component: SignupFormComponent }
])
//same as before...
ابتدا به RouteConfig، مسیریابی کامپوننت فرم افزودن کاربران، اضافه شده‌است. سپس ماژول این کلاس در ابتدای فایل import شده و در آخر routerLink آن به قالب سایت و منوی بالای سایت اضافه شده‌است.


معرفی کلاس FormBuilder

روش دیگری نیز برای ساخت ControlGroup و کنترل‌های آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
import { Control, ControlGroup, Validators, FormBuilder } from '@angular/common';

form: ControlGroup;
 
constructor(formBuilder: FormBuilder) {
    this.form = formBuilder.group({
        name: ['', Validators.required],
        email: ['', Validators.required],
        password: ['', Validators.required]
    });
}
کلاس و سرویس FormBuilder نیز در ماژول angular/common@ قرار دارد. برای استفاده‌ی از آن، آن‌را در سازنده‌ی کلاس تزریق کرده و سپس از متد group آن استفاده می‌کنیم. نحوه‌ی تعریف کلی اعضای آن با اعضای ControlGroup یکی است؛ با این تفاوت که اینبار بجای ذکر new Control، یک آرایه تعریف می‌شود که دقیقا اعضای آن، همان پارامترهای شیء کنترل هستند. این روش در کل خلاصه‌تر است و در آن تعریف چندین گروه مختلف، ساده‌تر می‌باشد. همچنین با روش تزریق وابستگی‌های بکار رفته‌ی در این فریم ورک نیز سازگاری بیشتری دارد.


پیاده سازی اعتبارسنجی سفارشی

فرض کنید می‌خواهیم ورود نام کاربر‌های دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشه‌ی app\users اضافه کنید؛ با این محتوا:
import { Control } from '@angular/common';
 
export class UsernameValidators {
    static cannotContainSpace(control: Control) {
        if (control.value.indexOf(' ') >= 0) {
            return { cannotContainSpace: true };
        }
        return null;
    }
}
کلاس UsernameValidators می‌تواند شامل تمام اعتبارسنجی‌های سفارشی خاصیت نام کاربری باشد. به همین جهت نام آن جمع است و به s ختم شد‌ه‌است.
هر متد پیاده سازی کننده‌ی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف می‌شود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن می‌توان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شده‌است.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بوده‌است.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی می‌تواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.

برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شی‌ءایی را بازگشت می‌دهد:
 { required:true }
و یا اگر اعتبارسنج minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه می‌دهد:
{
  minlength : {
     requiredLength : 3,
     actualLength : 1
  }
}
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد.

پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفاده‌ی از آن، ابتدا ماژول آن‌را به ابتدای ماژول signup-form.component.ts اضافه می‌کنیم:
 import { UsernameValidators } from './usernameValidators';
پس از آن، شبیه به افزودن متد استاتیک توکار Validators.required، این متد جدید را به لیست اعتبارسنجی‌های خاصیت name اضافه می‌کنیم. از آنجائیکه پیشتر المان دوم این آرایه مقدار دهی شده‌است، برای ترکیب چندین اعتبارسنجی با هم، از متد Validators.compose که آرایه‌ای از متدهای اعتبارسنجی را قبول می‌کند، کمک خواهیم گرفت:
 name: ['', Validators.compose([Validators.required, UsernameValidators.cannotContainSpace])],

و مرحله‌ی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
<div class="form-group">
    <label form="name">Username</label>
    <input id="name" type="text" class="form-control"
           ngControl="name"
           #name="ngForm" />
    <div *ngIf="name.touched && name.errors">
        <label class="text-danger" *ngIf="name.errors.required">
            Username is required.
        </label>
        <label class="text-danger" *ngIf="name.errors.cannotContainSpace">
            Username can't contain space.
        </label>
    </div>
</div>
همانطور که در قسمت قبل نیز عنوان شد، چون اکنون به یک المان، بیش از یک اعتبارسنجی اعمال شده‌است، استفاده از خاصیت valid، بیش از اندازه عمومی بوده و باید از خاصیت errors استفاده کرد. به همین جهت این دو اعتبارسنجی را در یک div محصور کننده قرار می‌دهیم و در صورت وجود خطایی، خاصیت name.errors، دیگر نال نبوده و دو برچسب قرار گرفته‌ی در آن بر اساس شرط‌های ngIf آن، پردازش خواهند شد.
نام خاصیت بازگشت داده شده‌ی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.

به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
import {Control} from '@angular/common';
 
export class EmailValidators {
    static email(control: Control) {
        var regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        var valid = regEx.test(control.value);
        return valid ? null : { email: true };
    }
}
در ادامه آن‌را به لیست formBuilder.group افزوده و همچنین پیام اعتبارسنجی ویژه‌ای را نیز به قالب فرم اضافه کنید (کدهای کامل آن، در فایل zip انتهای بحث موجود است).


یک نکته

اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، می‌توان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.


اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)

یک سری از اعتبارسنجی‌ها را در سمت کلاینت می‌توان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجی‌ها در رده‌ی async validation قرار می‌گیرند.
سازنده‌ی شیء Control در AngularJS 2.0 که در مثال‌های بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول می‌کند:
 control(value: Object, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn) : Control
پیاده سازی async validators، بسیار شبیه به سایر اعتبارسنج‌ها هستند. اما از آنجائیکه نیاز به کار با سرور را دارند، استاتیک تعریف کردن آن‌ها، سبب قطع شدن دسترسی آن‌ها از context کلاس جاری شده و امکان تزریق وابستگی‌ها را از دست خواهیم داد. برای مثال دیگر نمی‌توان به سادگی، سرویس دریافت اطلاعات کاربران را در اینجا تزریق کرد. یک راه حل رفع این مشکل، تعریف همان متد اعتبارسنج در کلاس کامپوننت فرم است:
nameShouldBeUnique(control: Control) {
    let name: string = control.value;
    return new Promise((resolve) => {
        this._userService.isUserNameUnique(<IUser>{ "name": name }).subscribe(
            (result: IResult) => {
                resolve(                    
                    result.result ? null : { 'nameShouldBeUnique': true }
                );
            },
            error => {
                resolve(null);
            }
        );
    });
}
و سپس فراخوانی آن به صورت ذیل، به عنوان سومین عنصر آرایه‌ی تعریف شده:
this.form = _formBuilder.group({
    name: ['', Validators.compose([
        Validators.required,
        UsernameValidators.cannotContainSpace
    ]),
        (control: Control) => this.nameShouldBeUnique(control)],
در اینجا با استفاده از arrow functions، امکان دسترسی به این متد تعریف شده‌ی در سطح کلاس، که استاتیک هم نیست، وجود خواهد داشت. به این ترتیب دیگر context کلاس را از دست نداده‌ایم و اینبار می‌توان به this._userService، که آن‌را در ادامه تکمیل خواهیم کرد، بدون مشکلی دسترسی یافت.
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن می‌توان به مقدار وارد شده‌ی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجه‌ی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شده‌است و در حالت resolve، یعنی عملیات تکمیل شده و اکنون می‌خواهیم نتیجه‌ی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شده‌ی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شده‌است.

این Promise، از یک سرویس تعریف شده‌ی در فایل جدید user.service.ts استفاده می‌کند:
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { IUser } from  './user';
import { IResult } from './result';
 
@Injectable()
export class UserService {
    private _checkUserUrl = '/home/checkUser';
 
    constructor(private _http: Http) { }
 
    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
 
    isUserNameUnique(user: IUser): Observable<IResult> {
        let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
        return this._http.post(this._checkUserUrl, JSON.stringify(user), options)
            .map((response: Response) => <IResult>response.json())
            .do(data => console.log("User: " + JSON.stringify(data)))
            .catch(this.handleError);
    }
}
با نحوه‌ی تعریف سرویس‌ها و همچنین کار با سرور و دریافت اطلاعات، در قسمت‌های قبلی بیشتر آشنا شدیم. در اینجا یک درخواست get، به آدرس home/checkuser سرور، ارسال می‌شود. سپس نتیجه‌ی آن در قالب اینترفیس IResult بازگشت داده خواهد شد. این اینترفیس را در فایل result.ts به صورت ذیل تعریف کرده‌ایم:
export interface IResult {
    result: boolean;
}

کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام می‌دهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شده‌اند:
namespace MVC5Angular2.Controllers
{
    public class HomeController : Controller
    {
        [HttpPost]
        public ActionResult CheckUser(User user)
        {
            var isUnique = new { result = true };
            if (user.Name?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false)
            {
                isUnique = new { result = false };
            }
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(isUnique, new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
در اینجا اگر نام کاربری وارد شده مساوی Vahid بود، یک شیء anonymous، مطابق امضای اینترفیس IResult سمت کاربر (همان فایل result.ts عنوان شده) بازگشت داده می‌شود.

بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شده‌است. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجه‌ای را مطابق امضای قرارداد IResult سفارشی ما بازگشت می‌دهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازنده‌ی آن تزریق کرده‌ایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کننده‌ی توکار AngularJS 2.0 مقدار دهی کرده‌ایم. البته همانطور که در مبحث تزریق وابستگی‌ها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آن‌را در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
@Component({
    selector: 'signup-form',
    templateUrl: 'app/users/signup-form.component.html',
    providers: [ UserService ]
})
export class SignupFormComponent {
 
    constructor(private _formBuilder: FormBuilder, private _userService: UserService) {
پس از ترزیق وابستگی private _userService: UserService، اکنون این سرویس به سادگی و به حالت متداولی در متد nameShouldBeUnique(control: Control) قابل دسترسی خواهد بود و از آن می‌توان جهت اعتبارسنجی‌های غیرهمزمان استفاده کرد.

اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شده‌است، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
<div *ngIf="name.touched && name.errors">
    <label class="text-danger" *ngIf="name.errors.required">
        Username is required.
    </label>
    <label class="text-danger" *ngIf="name.errors.cannotContainSpace">
        Username can't contain space.
    </label>
    <label class="text-danger" *ngIf="name.errors.nameShouldBeUnique">
        This username is already taken.
    </label>
</div>
در ادامه اگر برنامه را اجرا کنید، با ورود نام کاربری Vahid، یک چنین پیام خطایی، مشاهده خواهد شد:



نمایش پیام loading در حین انجام اعتبارسنجی از راه دور

شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجه‌ی آن، یک پیام loading را نیز نمایش داد. برای انجام این‌کار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
<input id="name" type="text" class="form-control"
       ngControl="name"
       #name="ngForm" />
<div *ngIf="name.control.pending">
    Checking server, Please wait ...
</div>
در اینجا یک div جدید را ذیل المان ورود نام کاربری اضافه کرده‌ایم. همچنین نحوه‌ی نمایش آن‌را با دسترسی به متغیر name# و کنترل منتسب، به آن مدیریت می‌کنیم. اگر عملیات async ایی بر روی این کنترل در حال اجرا باشد، Promise تعریف شده، وضعیت pending را بازگشت می‌دهد. به همین جهت می‌توان از این خاصیت، جهت نمایش دادن یا مخفی کردن عبارت و یا تصویری استفاده کرد.

 
اعتبارسنجی ترکیبی در حین submit یک فرم

فرض کنید می‌خواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمه‌ی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسی‌های لازم در متد onSubmit، می‌توان با استفاده از متد find شیء form، به یکی از کنترل‌های فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
onSubmit(): void {
    console.log(this.form.value);
 
    this.form.find('name').setErrors({
        invalidData : true
    }); 
}
سپس در سمت قالب این کامپوننت، نحوه‌ی نمایش این اعتبارسنجی سفارشی، همانند قبل است:
<div *ngIf="name.touched && name.errors">
    <label class="text-danger" *ngIf="name.errors.invalidData">
        Check the inputs....
    </label>
</div>


اتصال المان‌های فرم به مدلی جهت ارسال به سرور

اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شده‌ی در فرم و تغییرات آن‌ها دسترسی دارد، می‌توان از آن برای دسترسی به شیء‌ایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
  { name="VahidN", email="email@site.com", password="123"}
بنابراین برای ارسال اطلاعات این فرم به سرور، تنها کافی است این شیء را ارسال کنیم. به همین جهت در فایل user.service.ts، به کلاس سرویس کاربران، متد addUser را اضافه می‌کنیم:
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { Headers, RequestOptions } from '@angular/http';
import { IUser } from  './user';
import { IResult } from './result';
 
@Injectable()
export class UserService {
    private _addUserUrl = '/home/addUser';
 
    constructor(private _http: Http) { }
 
    private handleError(error: Response) {
        console.error(error);
        return Observable.throw(error.json().error || 'Server error');
    }
 
    addUser(user: IUser): Observable<IUser> {
        let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC
        let options = new RequestOptions({ headers: headers });
 
        return this._http.post(this._addUserUrl, JSON.stringify(user), options)
            .map((response: Response) => <IUser>response.json())
            .do(data => console.log("User: " + JSON.stringify(data)))
            .catch(this.handleError);
    }
}
کدهای سمت سرور آن در فایل Controllers\HomeController.cs نیز چنین شکلی را می‌توانند داشته باشند:
[HttpPost]
public ActionResult AddUser(User user)
{
    user.Id = 1; //todo: save user and get id from db
 
    return new ContentResult
    {
        Content = JsonConvert.SerializeObject(user, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        }),
        ContentType = "application/json",
        ContentEncoding = Encoding.UTF8
    };
}
و پس از آن کدهای متد onSubmit فایل signup-form.component.ts برای ارسال این شیء به صورت ذیل خواهند بود:
onSubmit(): void {
    console.log(this.form.value);
 
    /*this.form.find('name').setErrors({
            invalidData : true
        });*/
 
    this._userService.addUser(<IUser>this.form.value)
        .subscribe((user: IUser) => {
            console.log(`ID: ${user.id}`);
        });
}


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip


خلاصه‌ی بحث

برای اینکه بتوان کنترل بیشتری را بر روی المان‌های فرم داشت، ابتدا سرویس FormBuilder را در سازنده‌ی کلاس کامپوننت فرم تزریق می‌کنیم. سپس با استفاده از متد group آن، المان‌های فرم را به صورت کلیدهای شیء پارامتر آن تعریف می‌کنیم. در اینجا می‌توان اعتبارسنجی‌های توکار AngularJS 2.0 را که در کلاس پایه‌ی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آن‌ها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجی‌های async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل می‌شود و از تغییرات آن آگاه خواهد شد.
مطالب
معرفی Xamarin و مزیت‌های استفاده از آن

چرا برنامه نویسی موبایل؟

با افزایش روزافزون SmartPhone ها و تبلت‌ها، بازار تکنولوژی به این سمت سوق پیدا کرده‌است. از این رو شرکت‌های ارائه دهنده نرم افزاری، از این موقعیت استفاده کرده و هر کدام پلتفرم متفاوتی را برای برنامه نویسی بر روی این اسمارت فون‌ها ارائه داده‌اند. یکی از بزرگترین دغدغه‌های امروزه شرکت‌های برنامه نویسی و توسعه نرم افزار موبایل، انتخاب درست پلتفرم برای توسعه نرم افزار میباشد. در این مقاله قصد دارم یکی از این پلت فرم‌ها را بررسی کرده و معرفی کنم.

شرکت xamarin کار خود را در سال 2011 با ارایه نسخه Cross Platform پلتفرم .Net به نام Mono آغاز کرد. بعد از ارایه این نسخه از .Net، زامارین به کمک Mono توانست پیاده سازی بر روی  Android و IOS را به نام‌های MonoForAndroid  و MonoTouch ارایه دهد. بعد از این نسخه‌ها برنامه نویسان توانستند بر روی سیستم عامل‌های اندروید و آی او اس به صورت Native کد خود را به زبان C# نوشته و آن‌ها را اجرا کنند.


چرا باید از Xamarin استفاده کنم؟

در ادامه مقاله قصد دارم شما را با برخی از ویژگی‌های زامارین آشنا کرده و مزایای استفاده از آن را بیان کنم. 

Xamarin امکانی را فراهم کرده‌است که برنامه نویسان به دو روش متفاوت قادر خواهند بود برنامه‌های خود را بنویسند:


Xamarin Native :1

زامارین به شما این امکان را میدهد که بتوانید به صورت مستقیم برای هر پلتفرم به صورت جداگانه برنامه نویسی کنید. در اندروید اینکار با Xamarin.Droid و در IOS اینکار با Xamarin.Touch امکانپذیر است. مزیت‌های استفاده از این روش عبارتند از:

·   بهره گیری از یک زبان برنامه نویسی

همانطور که میدانید یادگیری یک زبان برنامه نویسی هزینه‌ی زیادی را برای یک سازمان و یا یک شخص به همراه دارد. در زامارین این امکان فراهم شده‌است که با استفاده از تنها یک زبان برنامه نویسی مانند C# ، برنامه نویسان بتوانند برای پلتفرم‌های مختلف برنامه بنویسند. در نظر داشته باشید که UI در هر پلتفرم به صورت جداگانه پیاده سازی میشود. به طور مثال در اندروید به وسیله‌ی Android Xml میتوانید ظاهر برنامه خود را پیاده سازی کنید و منطق‌های خود را با زبان C# برای تمامی پلت فرم‌ها به صورت یکسان بنویسید.

·   تجربه کاربری Native

زامارین به شما این امکان را خواهد داد که با استفاده از کنترل‌های Native هر پلتفرم به تجربه کاربری همان پلت فرم دسترسی پیدا کنید و اپلیکیشنی با ظاهر و UX همان پلتفرم بسازید.

·   استفاده  100% از امکانات هر پلتفرم

زامارین به دلیل Native بودن این امکان را به برنامه نویسان ارائه میدهد که با استفاده از یک زبان و با بکارگیری Cycle  Life مخصوص هر پلتفرم،  به 100% امکانات و API ‌های هر پلتفرم دسترسی پیدا کنند.

·   Performance

به دلیل اینکه برنامه‌های زامارین به صورت Native اجرا میشوند Performance بالایی دارند.

·   دسترسی به API ‌های موجود در .Net

شما قادر خواهید بود با دانش موجود مانند Entity Framework Code و.. به فریم ورک .Net  دسترسی پیدا کرده و از API ‌های درون آن استفاده کنید. زامارین از یکی از پیاده سازی‌های .Net Standard استفاده میکند.

·   استفاده مجدد از کد

در زامارین قادر خواهید بود که از کدهای خود، استفاده مجدد کرده و این امر سبب مدیریت بهتر بر روی کد، کد نویسی کمتر، هزینه نگهداری کد کمتر، توسعه راحت‌تر اپلیکیشن و ... میشود.

·   تست خودکار

در زامارین شما میتوانید برای کدهای خود تست خودکار نوشته و آنها را به صورت خودکار تست کنید.

·   Bind کردن Library ‌های Objective-C و Java

زامارین طوری طراحی شده‌است که دست شما را در هیچگونه شرایطی نخواهد بست. شما میتوانید به صورت مستقیم کدهایی را که به زبان های Java  و Objective-C نوشته شده‌اند، به پروژه اضافه کرده و هیچگونه نگرانی از بابت کدهای از قبل نوشته شده که به زبان‌های Objective-C و Java هستند، نداشته باشید.

·   Designer

در زمارین این امکان وجود دارد که در هر پلتفرم از طریق Designer مخصوص به آن پلتفرم، UI خود را طراحی و پیاده سازی کنید.

·   Async

  در برنامه نویسی غیر همزمان ( Asynchronous Programming ) این امکان وجود دارد که برنامه شما بدون توقف، یک قسمت از کد را اجرا کرده و منتظر اجرای قسمت‌های دیگر کد نشود؛ یا به اصطلاح برنامه از حالت Response خارج نشود. در زبان‌های Java ، Objective-C و Swift اینکار باید با CallBack و به صورت Manual مدیریت شود؛ اما #C این امکان را فراهم آورده است که به راحتی اینکار را انجام داده و برنامه خود را همیشه در حالت پاسخ دهی نگه دارید.  

public async Task<List<FeedItem>> GetFeedItems(DateTime date) {
  var feed = "http://planet.xamarin.com/feed/";
  var response = await httpClient.GetStringAsync(feed);
  var items = await ParseFeedAsync(response);
  return items.Where(item => item.Published.Date == date).ToList();
}


·   Parallel Programming

در برنامه نویسی موازی( Parallel Programming )  برخلاف برنامه نویسی MultiThread که بر روی یک هسته CPU اجرا میشود، بر روی چندین هسته CPU به صورت موازی اجرا میشود. زامارین از این نوع برنامه نویسی پشتیبانی میکند.


Xamarin.Forms: 2

پس از معرفی Xamarin Forms API شما میتوانید علاوه بر مزیت‌هایی که در بالا اشاره شد، کدهای Logic خود را با زبان C# و کدهای UI خود را با زبان XAML پیاده سازی کرده و با یک بار نوشتن کد، در پلتفرم‌های مختلف خروجی خود را مشاهده کنید.  مزیت استفاده از Xamarin Forms عبارتند از:

·   استفاده از کد واحد برای پیاده سازی UI و Logic

یکی از بهترین مزیت‌هایی را که میتوان به آن اشاره نمود این است که شما کافیست یک بار کد خود را بنویسید و Xamarin Forms کد شما را در پلت فرم‌های متفاوت پیاده سازی خواهد کرد.

<?xml version="1.0" encoding="UTF-8"?>
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
            xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
            x:Class="MyApp.MainPage">
    <TabbedPage.Children>
        <ContentPage Title="Profile" Icon="Profile.png">
            <StackLayout Spacing="20" Padding="20"
                         VerticalOptions="Center">
                <Entry Placeholder="Username"
                       Text="{Binding Username}"/>
                <Entry Placeholder="Password"
                       Text="{Binding Password}"
                       IsPassword="true"/>
                <Button Text="Login" TextColor="White"
                        BackgroundColor="#77D065"
                        Command="{Binding LoginCommand}"/>
            </StackLayout>
        </ContentPage>
        <ContentPage Title="Settings" Icon="Settings.png">
            <!-- Settings -->
        </ContentPage>
    </TabbedPage.Children>
</TabbedPage>


برای پیاده سازی UI در Xamarin Forms باید از XAML استفاده کنید. همچنین مانند روش قبلی میتوانید از زبان C# برای پیاده سازی منطق، استفاده نمایید.

با استفاده از Xamarin Forms شما تجربه نوشتن کد Cross Platform را در کنار Native بودن آن خواهید داشت.

·   استفاده از یک کنترل مخصوص یک پلتفرم در بین کد ( Embedding )

در Xamarin Forms این امکان وجود دارد که اگر شما خواستید یک کنترل مخصوص IOS را در بین کدهای خود استفاده کنید، بتوانید به راحتی اینکار را انجام دهید.( (Embedding

اگر به تصویر بالا دقت کنید متوجه خواهید شد که در یکسری از کنترل‌ها، تصاویر متفاوت هستند. در نسخه اندروید یک Action Button در قسمت پایین صفحه مشاهده میکنید که در نسخه‌ی IOS آن موجود نیست. یعنی به صورت مستقیم کنترل Action Button که مختص به پلت فرم اندروید میباشد، درون Xamarin Forms استفاده شده است.

·   دسترسی به هر پلتفرم به طور مستقیم

شما قادر خواهید بود به طور مستقیم به هر پلت فرم دسترسی پیدا کرده و به طور مثال در هر پلتفرم، UI مخصوص به خود را با Property ‌های مخصوص به خود طراحی کنید.

·   UITest و Test Cloud

در Xamarin Forms  میتوانید برای UI خود تست نوشته و آن‌ها را به وسیله Xamarin Test Cloud بر روی صدها Device متفاوت تست کنید. (این امکان فقط برای Android و IOS وجود دارد.)

·   Life Cycle مشابه در تمامی پلتفرم ها

همانطور که میدانید پلتفرم‌های مختلف، Life Cycle ‌های متفاوتی برای مدیریت اپلیکیشن دارند. یکی از مزیت‌های استفاده از Xamarin Forms این است که شما میتوانید برای تمامی پلتفرم‌ها به‌وسیله‌ی یک Life Cycle یکسان کد بنویسید.

·   Previewer

یکی از بهترین قابلیت‌هایی که در Xamarin Forms اضافه شده‌است این است که شما قادر خواهید بود به صورت Real Time خروجی فایل XAML خود را به وسیله Previewer مشاهده نمایید.

·   Performance Profiler

به وسیله Xamarin Profiler شما میتوانید میزان مصرف حافظه، Performance و ... را در اندروید و IOS اندازه گیری نمایید.

نکات قابل توجه:

Ø   استفاده همزمان از Xamarin Forms و Xamarin Native

شما میتوانید کدهای خود را با حداکثر Reusability نوشته و در صورت لزوم با کدهای Xamarin Native ترکیب کنید.

Ø   Documentation خیلی خوب

زمارین  مستندات جامع و کاملی را در سایت خود گردآوری کرده که میتوانید به راحتی از آن برای فهم تمامی قسمت‌های Xamarin استفاده کنید.