- بروز رخدادهای DOM مانند کلیک
- صدور درخواستهای Ajax ایی
- استفاده از تایمرها (setTimer, setInterval)
ردیابهای تغییرات در Angular
تمام برنامههای Angular در حقیقت سلسله مراتبی از کامپوننتها هستند. در زمان اجرای برنامه، Angular به ازای هر کامپوننت، یک تشخیص دهندهی تغییرات را ایجاد میکند که در نهایت سلسله مراتب ردیابها را همانند سلسله مراتب کامپوننتها ایجاد خواهد کرد. هر زمانیکه ردیابی فعال میشود، Angular این درخت را پیموده و مواردی را که تغییراتی را گزارش دادهاند، بررسی میکند. این پیمایش به ازای هر تغییر رخ دادهی در مدلهای برنامه صورت میگیرد و همواره از بالای درخت شروع شده و به صورت ترتیبی تا پایین آن ادامه پیدا میکند:
این پیمایش ترتیبی از بالا به پایین، از این جهت صورت میگیرد که اطلاعات کامپوننتها از والدین آنها تامین میشوند. تشخیص دهندههای تغییرات، روشی را جهت ردیابی وضعیت پیشین و فعلی یک کامپوننت ارائه میدهد تا Angular بتواند تغییرات رخداده را منعکس کند. اگر Angular گزارش تغییری را از یک تشخیص دهندهی تغییر دریافت کند، کامپوننت مرتبط را مجددا ترسیم کرده و DOM را به روز رسانی میکند.
استراتژیهای تشخیص تغییرات در Angular
برای درک نحوهی عملکرد سیستم تشخیص تغییرات نیاز است با مفهوم value types و reference types در JavaScript آشنا شویم. در JavaScript نوعهای زیر value type هستند:
• Boolean • Null • Undefined • Number • String
• Arrays • Objects • Functions
این دو تفاوت را میتوان در شکل زیر بهتر مشاهده کرد:
استراتژی Default یا پیشفرض تشخیص تغییرات در Angular
همانطور که پیشتر نیز عنوان شد، Angular تغییرات یک شیء مدل را در جهت تشخیص تغییرات و انعکاس آنها به View برنامه، ردیابی میکند. در این حالت هر تغییری بین حالت فعلی و پیشین یک شیء مدل برای این منظور بررسی میگردد. در اینجا Angular این سؤال را مطرح میکند: آیا مقداری در این مدل تغییر یافتهاست؟
اما برای یک reference type میتوان سؤالات بهتری را مطرح کرد که بهینهتر و سریعتر باشند. اینجاست که استراتژی OnPush تشخیص تغییرات مطرح میشود.
استراتژی OnPush تشخیص تغییرات در Angular
ایده اصلی استراتژی OnPush تشخیص تغییرات در Angular در immutable فرض کردن reference types نهفتهاست. در این حالت هر تغییری در شیء مدل، سبب ایجاد یک ارجاع جدید به آن در stack memory میشود. به این ترتیب میتوان تشخیص تغییرات بسیار سریعتری را شاهد بود. چون دیگر در اینجا نیازی نیست تمام مقادیر یک شیء را مدام تحت نظر قرار داد. همینقدر که ارجاع آن در stack memory تغییر کند، یعنی مقادیر این شیء در heap memory تغییر یافتهاند.
در این حالت Angular دو سؤال را مطرح میکند: آیا ارجاع به یک reference type در stack memory تغییر یافتهاست؟ اگر بله، آیا مقادیر آن در heap memory تغییر کردهاند؟ برای مثال جهت بررسی تغییرات یک آرایهی با 30 عضو، دیگر در ابتدای کار نیازی نیست تا هر 30 عضو آن بررسی شوند (برخلاف حالت پیشفرض بررسی تغییرات). در حالت استراتژی OnPush، ابتدا مقدار ارجاع این آرایه در stack memory بررسی میشود. اگر تغییری در آن صورت گرفته بود، به معنای تغییری در اعضای آرایهاست.
استراتژی OnPush در یک کامپوننت به نحو ذیل فعال و انتخاب میشود و مقدار پیشفرض آن ChangeDetectionStrategy.Default است:
import {ChangeDetectionStrategy, Component} from '@angular/core'; @Component({ // ... changeDetection: ChangeDetectionStrategy.OnPush }) export class OnPushComponent { // ... }
- اگر مقدار یک خاصیت از نوع Input@ تغییر کند.
- اگر یک event handler رخدادی را صادر کند.
- اگر سیستم ردیابی به صورت دستی فراخوانی شود.
- اگر سیستم ردیاب تغییرات child component آن، اجرا شود.
نوعهای ارجاعی Immutable
همانطور که عنوان شد، شرط کار کردن استراتژی OnPush، داشتن نوعهای ارجاعی immutable است. اما نوع ارجاعی immutable چیست؟
Immutable بودن به زبان ساده به این معنا است که ما هیچگاه جهت تغییر مقدار خاصیتی در یک شیء، آن خاصیت را مستقیما مقدار دهی نمیکنیم؛ بلکه کل شیء را مجددا مقدار دهی میکنیم.
برای نمونه در مثال زیر، خاصیت foo شیء before مستقیما مقدار دهی شدهاست:
static mutable() { var before = {foo: "bar"}; var current = before; current.foo = "hello"; console.log(before === current); // => true }
static immutable() { var before = {foo: "bar"}; var current = before; current = {foo: "hello"}; console.log(before === current); // => false }
معرفی کتابخانهی Immutable.js
جهت ایجاد اشیاء واقعی immutable کتابخانهی Immutable.js توسط Facebook ایجاد شدهاست و برای کار با استراتژی تشخیص تغییرات OnPush در Angular بسیار مناسب است.
برای نصب آن دستور ذیل را صادر نمائید:
npm install immutable --save
import {Map, List} from 'immutable'; var foobar = {foo: "bar"}; var immutableFoobar = Map(foobar); console.log(immutableFooter.get("foo")); // => bar var helloWorld = ["Hello", "World!"]; var immutableHelloWorld = List(helloWorld); console.log(immutableHelloWorld.first()); // => Hello console.log(immutableHelloWorld.last()); // => World! helloWorld.push("Hello Mars!"); console.log(immutableHelloWorld.last()); // => Hello Mars!
تغییر ارجاع به یک شیء با استفاده از spread properties
const user = { name: 'Max', age: 30 } user.age = 31
اگر علاقمند به استفادهی از یک کتابخانهی اضافی مانند Immutable.js در کدهای خود نباشید، روش دیگری نیز برای تغییر ارجاع به یک شیء وجود دارد:
const user = { name: 'Max', age: 30 } const modifiedUser = { ...user, age: 31 }
نمونهی دیگر آن در حین کار با متد push یک آرایهاست:
export class AppComponent { foods = ['Bacon', 'Lettuce', 'Tomatoes']; addFood(food) { this.foods.push(food); } }
addFood(food) { this.foods = [...this.foods, food]; }
آگاه سازی دستی موتور تشخیص تغییرات Angular در حالت استفادهی از استراتژی OnPush
تا اینجا دریافتیم که استراتژی OnPush تنها به تغییرات ارجاعات به اشیاء پاسخ میدهد و به نحوی باید این ارجاع را با هر به روز رسانی تغییر داد. اما روش دیگری نیز برای وادار کردن این سیستم به تغییر وجود دارد:
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-child', templateUrl: './child.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class ChildComponent { @Input() data: string[]; constructor(private cd: ChangeDetectorRef) {} refresh() { this.cd.detectChanges(); } }
کار با Observables در حالت استفادهی از استراتژی OnPush
مطلب «صدور رخدادها از سرویسها به کامپوننتها در برنامههای Angular» را در نظر بگیرید. Observables نیز ما را از تغییرات رخداده آگاه میکنند؛ اما برخلاف immutable objects با هر تغییری که رخ میدهد، ارجاع به آنها تغییری نمیکند. آنها تنها رخدادی را به مشترکین، جهت اطلاع رسانی از تغییرات صادر میکنند.
بنابراین اگر از Observables و استراتژی OnPush استفاده کنیم، چون ارجاع به آنها تغییری نمیکند، رخدادهای صادر شدهی توسط آنها ردیابی نخواهند شد. برای رفع این مشکل، امکان علامتگذاری رخدادهای Observables به تغییر کرده پیشبینی شدهاست.
در اینجا کامپوننتی را داریم که قابلیت صدور رخدادها را از طریق یک BehaviorSubject دارد:
import { Component } from '@angular/core'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; @Component({ ... }) export class AppComponent { foods = new BehaviorSubject(['Bacon', 'Letuce', 'Tomatoes']); addFood(food) { this.foods.next(food); } }
import { Component, Input, ChangeDetectionStrategy, ChangeDetectorRef, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; @Component({ selector: 'app-child', templateUrl: './child.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) export class ChildComponent implements OnInit { @Input() data: Observable<any>; foods: string[] = []; constructor(private cd: ChangeDetectorRef) {} ngOnInit() { this.data.subscribe(food => { this.foods = [...this.foods, ...food]; }); }
ngOnInit() { this.data.subscribe(food => { this.foods = [...this.foods, ...food]; this.cd.markForCheck(); // marks path }); }
اگر در حال تهیه یک سایت چند زبانه هستید و همچنین سری مقالات Globalization در ASP.NET MVC رو دنبال کرده باشید میدانید که با تغییر Culture فایلهای Resource مورد نظر بارگذاری و نوشتههای سایت تغییر میابند ولی با تغییر Culture رفتار اعتبارسنجی در سمت سرور نیز تغییر و اعتبارسنجی بر اساس Culture فعلی سایت انجام میگیرد. بررسی این موضوع را با یک مثال شروع میکنیم.
یک پروژه وب بسازید سپس به پوشه Models یک کلاس با نام ValueModel اضافه کنید. تعریف کلاس به شکل زیر هست:
public class ValueModel { [Required] [Display(Name = "Decimal Value")] public decimal DecimalValue { get; set; } [Required] [Display(Name = "Double Value")] public double DoubleValue { get; set; } [Required] [Display(Name = "Integer Value")] public int IntegerValue { get; set; } [Required] [Display(Name = "Date Value")] public DateTime DateValue { get; set; } }
به سراغ کلاس HomeController بروید و کدهای زیر را اضافه کنید:
[HttpPost] public ActionResult Index(ValueModel valueModel) { if (ModelState.IsValid) { return Redirect("Index"); } return View(valueModel); }
Culture را به fa-IR تغییر میدهیم، برای اینکار در فایل web.config در بخش system.web کد زیر اضافه نمایید:
<globalization culture="fa-IR" uiCulture="fa-IR" />
و در نهایت به سراغ فایل Index.cshtml بروید کدهای زیر رو اضافه کنید:
@using (Html.BeginForm()) { <ol> <li> @Html.LabelFor(m => m.DecimalValue) @Html.TextBoxFor(m => m.DecimalValue) @Html.ValidationMessageFor(m => m.DecimalValue) </li> <li> @Html.LabelFor(m => m.DoubleValue) @Html.TextBoxFor(m => m.DoubleValue) @Html.ValidationMessageFor(m => m.DoubleValue) </li> <li> @Html.LabelFor(m => m.IntegerValue) @Html.TextBoxFor(m => m.IntegerValue) @Html.ValidationMessageFor(m => m.IntegerValue) </li> <li> @Html.LabelFor(m => m.DateValue) @Html.TextBoxFor(m => m.DateValue) @Html.ValidationMessageFor(m => m.DateValue) </li> <li> <input type="submit" value="Submit"/> </li> </ol> }
پرژه را اجرا نمایید و در ٢ تکست باکس اول ٢ عدد اعشاری را و در ٢ تکست باکس آخر یک عدد صحیح و یک تاریخ وارد نمایید و سپس دکمه Submit را بزنید. پس از بازگشت صفحه از سمت سرور در در ٢ تکست باکس اول با این پیامها روبرو میشوید که مقادیر وارد شده نامعتبر میباشند.
اگر پروژه رو در حالت دیباگ اجرا کنیم و نگاهی به داخل ModelState بیاندازیم، میبینیم که کاراکتر جدا کننده قسمت اعشاری برای fa-IR '/' میباشد که در اینجا برای اعداد مورد نظر کاراکتر '.' وارد شده است.
برای فایق شدن بر این مشکل یا باید سمت سرور اقدام کرد یا در سمت کلاینت. در بخش اول راه حل سمت کلاینت را بررسی مینماییم.
در سمت کلاینت برای اینکه کاربر را مجبور به وارد کردن کاراکترهای مربوط به Culture فعلی سایت نماییم باید مقادیر وارد شده را اعتبارسنجی و در صورت معتبر نبودن مقادیر پیام مناسب نشان داده شود. برای اینکار از کتابخانه jQuery Globalize استفاده میکنیم. برای اضافه کردن jQuery Globalize از طریق کنسول nuget فرمان زیر اجرا نمایید:
PM> Install-Package jquery-globalize
پس از نصب کتابخانه اگر به پوشه Scripts نگاهی بیاندازید میبینید که پوشەای با نام jquery.globalize اضافه شده است. درداخل پوشه زیر پوشەی دیگری با نام cultures وجود دارد که در آن Cultureهای مختلف وجود دارد و بسته به نیاز میتوان از آنها استفاده کرد. دوباره به سراغ فایل Index.cshtm بروید و فایلهای جاوا اسکریپتی زیر را به صفحه اضافه کنید:
<script src="~/Scripts/jquery.validate.js"> </script> <script src="~/Scripts/jquery.validate.unobtrusive.js"> </script> <script src="~/Scripts/jquery.globalize/globalize.js"> </script> <script src="~/Scripts/jquery.globalize/cultures/globalize.culture.fa-IR.js"> </script>
در فایل globalize.culture.fa-IR.js کاراکتر جدا کننده اعشاری '.' در نظر گرفته شده است که مجبور به تغییر آن هسیتم. برای اینکار فایل را باز کرده و numberFormat را پیدا کنید و آن را به شکل زیر تغییر دهید:
numberFormat: { pattern: ["n-"], ".": "/", currency: { pattern: ["$n-", "$ n"], ".": "/", symbol: "ریال" } },
و در نهایت کدهای زیر را به فایل Index.cshtml اضافه کنید و برنامه را دوباره اجرا نمایید:
Globalize.culture('fa-IR'); $.validator.methods.number = function(value, element) { if (value.indexOf('.') > 0) { return false; } var splitedValue = value.split('/'); if (splitedValue.length === 1) { return !isNaN(Globalize.parseInt(value)); } else if (splitedValue.length === 2 && $.trim(splitedValue[1]).length === 0) { return false; } return !isNaN(Globalize.parseFloat(value)); }; };
در خط اول Culture را ست مینمایم و در ادامه نحوه اعتبارسنجی را در unobtrusive validation تغییر میدهیم. از آنجایی که برای اعتبارسنجی عدد وارد شده از تابع parseFloat استفاده میشود، کاراکتر جدا کننده قسمت اعشاری قابل قبول برای این تابع '.' است پس در داخل تابع دوباره '/' به '.' تبدیل میشود و سپس اعتبارسنجی انجام میشود از اینرو اگر کاربر '.' را نیز وارد نماید قابل قبول است به همین دلیل با این خط کد if (value.indexOf('.') > 0) وجود نقطه را بررسی میکنیم تا در صورت وجود '.' پیغام خطا نشان داده شود.در خط بعدی بررسی مینماییم که اگر عدد وارد شده اعشاری نباشد از تابع parseInt استفاده نماییم. در خط بعدی این حالت را بررسی مینماییم که اگر کاربر عددی همچون /١٢ وارد کرد پیغام خطا صادر شود.
برای اعتبارسنجی تاریخ شمسی متاسفانه توابع کمکی برای تبدیل تاریخ در فایل globalize.culture.fa-IR.js وجود ندارد ولی اگر نگاهی به فایلهای Culture عربی بیاندازید همه دارای توابع کمکی برای تبدیل تاریج هجری به میلادی هستند به همین دلیل امکان اعتبارسنجی تاریخ شمسی با استفاده از jQuery Globalize میسر نمیباشد. من خودم تعدادی توابع کمکی را به globalize.culture.fa-IR.js اضافه کردەام که از تقویم فارسی آقای علی فرهادی برداشت شده است و با آنها کار اعتبارسنجی را انجام میدهیم. لازم به ذکر است این روش ١٠٠% تست نشده است و شاید راه کاملا اصولی نباشد ولی به هر حال در اینجا توضیح میدهم. در فایل globalize.culture.fa-IR.js قسمت Gregorian_Localized را پیدا کنید و آن را با کدهای زیر جایگزین کنید:
Gregorian_Localized: { firstDay: 6, days: { names: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesAbbr: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesShort: ["ی", "د", "س", "چ", "پ", "ج", "ش"] }, months: { names: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""], namesAbbr: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""] }, AM: ["ق.ظ", "ق.ظ", "ق.ظ"], PM: ["ب.ظ", "ب.ظ", "ب.ظ"], patterns: { d: "yyyy/MM/dd", D: "yyyy/MM/dd", t: "hh:mm tt", T: "hh:mm:ss tt", f: "yyyy/MM/dd hh:mm tt", F: "yyyy/MM/dd hh:mm:ss tt", M: "dd MMMM" }, JalaliDate: { g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] }, gregorianToJalali: function (gY, gM, gD) { gY = parseInt(gY); gM = parseInt(gM); gD = parseInt(gD); var gy = gY - 1600; var gm = gM - 1; var gd = gD - 1; var gDayNo = 365 * gy + parseInt((gy + 3) / 4) - parseInt((gy + 99) / 100) + parseInt((gy + 399) / 400); for (var i = 0; i < gm; ++i) gDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i]; if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0) || (gy % 400 == 0))) /* leap and after Feb */ ++gDayNo; gDayNo += gd; var jDayNo = gDayNo - 79; var jNp = parseInt(jDayNo / 12053); jDayNo %= 12053; var jy = 979 + 33 * jNp + 4 * parseInt(jDayNo / 1461); jDayNo %= 1461; if (jDayNo >= 366) { jy += parseInt((jDayNo - 1) / 365); jDayNo = (jDayNo - 1) % 365; } for (var i = 0; i < 11 && jDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; ++i) { jDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; } var jm = i + 1; var jd = jDayNo + 1; return [jy, jm, jd]; }, jalaliToGregorian: function (jY, jM, jD) { jY = parseInt(jY); jM = parseInt(jM); jD = parseInt(jD); var jy = jY - 979; var jm = jM - 1; var jd = jD - 1; var jDayNo = 365 * jy + parseInt(jy / 33) * 8 + parseInt((jy % 33 + 3) / 4); for (var i = 0; i < jm; ++i) jDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; jDayNo += jd; var gDayNo = jDayNo + 79; var gy = 1600 + 400 * parseInt(gDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */ gDayNo = gDayNo % 146097; var leap = true; if (gDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ { gDayNo--; gy += 100 * parseInt(gDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */ gDayNo = gDayNo % 36524; if (gDayNo >= 365) gDayNo++; else leap = false; } gy += 4 * parseInt(gDayNo / 1461); /* 1461 = 365*4 + 4/4 */ gDayNo %= 1461; if (gDayNo >= 366) { leap = false; gDayNo--; gy += parseInt(gDayNo / 365); gDayNo = gDayNo % 365; } for (var i = 0; gDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap) ; i++) gDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap); var gm = i + 1; var gd = gDayNo + 1; return [gy, gm, gd]; }, checkDate: function (jY, jM, jD) { return !(jY < 0 || jY > 32767 || jM < 1 || jM > 12 || jD < 1 || jD > (Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[jM - 1] + (jM == 12 && !((jY - 979) % 33 % 4)))); }, convert: function (value, format) { var day, month, year; var formatParts = format.split('/'); var dateParts = value.split('/'); if (formatParts.length !== 3 || dateParts.length !== 3) { return false; } for (var j = 0; j < formatParts.length; j++) { var currentFormat = formatParts[j]; var currentDate = dateParts[j]; switch (currentFormat) { case 'dd': if (currentDate.length === 2 || currentDate.length === 1) { day = currentDate; } else { year = currentDate; } break; case 'MM': month = currentDate; break; case 'yyyy': if (currentDate.length === 4) { year = currentDate; } else { day = currentDate; } break; default: return false; } } year = parseInt(year); month = parseInt(month); day = parseInt(day); var isValidDate = Globalize.culture().calendars.Gregorian_Localized.checkDate(year, month, day); if (!isValidDate) { return false; } var grDate = Globalize.culture().calendars.Gregorian_Localized.jalaliToGregorian(year, month, day); var shDate = Globalize.culture().calendars.Gregorian_Localized.gregorianToJalali(grDate[0], grDate[1], grDate[2]); if (year === shDate[0] && month === shDate[1] && day === shDate[2]) { return true; } return false; } },
روال کار در تابع convert به اینصورت است که ابتدا تاریخ وارد شده را بررسی مینماید تا معتبر بودن آن معلوم شود به عنوان مثال اگر تاریخی مثل 1392/12/31 وارد شده باشد و در ادامه برای بررسی بیشتر تاریخ یک بار به میلادی و تاریخ میلادی دوباره به شمسی تبدیل میشود و با تاریخ وارد شده مقایسه میشود و در صورت برابری تاریخ معتبر اعلام میشود. در فایل Index.cshtml کدهای زیر اضافی نمایید:
$.validator.methods.date = function (value, element) { return Globalize.culture().calendars.Gregorian_Localized.convert(value, 'yyyy/MM/dd'); };
برای اعتبارسنجی تاریخ میتوانید از ٢ فرمت استفاده کنید:
١ – yyyy/MM/dd
٢ – dd/MM/yyyy
البته از توابع اعتبارسنجی تاریخ میتوانید به صورت جدا استفاده نمایید و لزومی ندارد آنها را همراه با jQuery Globalize بکار ببرید. در آخر خروجی کار به این شکل است:
در کل استفاده از jQuery Globalize برای اعتبارسنجی در سایتهای چند زبانه به نسبت خوب میباشد و برای هر زبان میتوانید از culture مورد نظر استفاده نمایید. در قسمت دوم این مطلب به بررسی بخش سمت سرور میپردازیم.
کتابخانه angular-modal-alerts
کتابخانه angular-colorpicker
The color picker for AngularJS - Native / Simple / Cool. No other dependencies are needed at all. Demo
static void Main() { int factor = 2; Func<int, int> multiplier = n => n * factor; Console.WriteLine (multiplier (3)); // 6 }
عبارت لامبدا زمانی ارزیابی میشود که delegate متناظر فراخوانی (Invoke) گردد؛ نه زمانیکه متغیر اصطلاحا capture میشود:
int factor = 2; Func<int, int> multiplier = n => n * factor; factor = 10; Console.WriteLine (multiplier (3)); // 30
عبارات لامبدا خود میتوانند captured variableها را تغییر دهند:
int seed = 0; Func<int> natural = () => seed++; Console.WriteLine (natural()); // 0 Console.WriteLine (natural()); // 1 Console.WriteLine (seed); // 2
static Func<int> Natural() { int seed = 0; return () => seed++; // Returns a closure } static void Main() { Func<int> theNatural = Natural(); Console.WriteLine (theNatural ()); // 0 Console.WriteLine (theNatural ()); // 1 }
static Func<int> Natural() { return() => { int seed = 0; return seed++; }; } static void Main() { Func<int> natural = Natural(); Console.WriteLine (natural()); // 0 Console.WriteLine (natural()); // 0 }
Action[] actions = new Action[3]; for (int i = 0; i < 3; i++) actions [i] = () => Console.Write (i); foreach (Action a in actions) a(); // 333
Action[] actions = new Action[3]; int i = 0; actions[0] = () => Console.Write (i); i = 1; actions[1] = () => Console.Write (i); i = 2; actions[2] = () => Console.Write (i); i = 3; foreach (Action a in actions) a(); // 333
Action[] actions = new Action[3]; for (int i = 0; i < 3; i++) { int loopScopedi = i; actions [i] = () => Console.Write (loopScopedi); } foreach (Action a in actions) a(); // 012
- Velocity CTP3 ارائه شد.
پروژه سورس بازی است که از بسیاری از ماژولهای جدید دات نت فریم ورک استفاده میکند و نکتهی جالب نگارش جدید آن مهاجرت از MS Ajax به jQuery Ajax است (قسمت اجکس آن کلا از صفر بر اساس jQuery بازنویسی شده است).
- چک لیستی که پیش از برپایی یک وب سایت باید بررسی شود.
- نگارش بتا Ext Core 3.0 ارائه شد.
- یک سری برگههای مرجع لینوکسی!
- دمویی در مورد ویندوز Azure و سرویسهای مبتنی بر آن
این سری شامل بررسی موارد ذیل خواهد بود:
- قسمت اول - معرفی و ایجاد ساختار برنامه
- قسمت دوم - سرویس اعتبارسنجی
- قسمت سوم - ورود به سیستم
- قسمت چهارم - به روز رسانی خودکار توکنها
- قسمت پنجم - محافظت از مسیرها
- قسمت ششم - کار با منابع محافظت شدهی سمت سرور
پیشنیازها
- آشنایی با Angular CLI
- آشنایی با مسیریابیها در Angular
- آشنایی با فرمهای مبتنی بر قالبها
همچنین اگر پیشتر Angular CLI را نصب کردهاید، قسمت «به روز رسانی Angular CLI» ذکر شدهی در مطلب «Angular CLI - قسمت اول - نصب و راه اندازی» را نیز اعمال کنید. در این سری از angular/cli: 1.6.0@ استفاده شدهاست.
ایجاد ساختار اولیه و مسیریابیهای آغازین مثال این سری
در ادامه، یک پروژهی جدید مبتنی بر Angular CLI را به نام ASPNETCore2JwtAuthentication.AngularClient به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
> ng new ASPNETCore2JwtAuthentication.AngularClient --routing
به علاوه، قصد استفادهی از بوت استرپ را نیز داریم. به همین جهت ابتدا به ریشهی پروژه وارد شده و سپس دستور ذیل را صادر کنید، تا بوت استرپ نصب شود و پرچم save آن سبب به روز رسانی فایل package.json نیز گردد:
> npm install bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
در ادامه برای تکمیل مثال جاری، دو کامپوننت جدید خوشآمدگویی و همچنین یافتن نشدن مسیرها را به برنامه اضافه میکنیم:
>ng g c welcome >ng g c PageNotFound
@NgModule({ declarations: [ AppComponent, WelcomeComponent, PageNotFoundComponent ],
import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { WelcomeComponent } from './welcome/welcome.component'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = [ { path: 'welcome', component: WelcomeComponent }, { path: '', redirectTo: 'welcome', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
همچنین مدیریت مسیریابی آدرسهای ناموجود در سایت نیز با تعریف ** صورت گرفتهاست.
ایجاد ماژول Authentication و تعریف کامپوننت لاگین
کامپوننتهای احراز هویت و اعتبارسنجی کاربران را در ماژولی به نام Authentication قرار خواهیم داد. بنابراین ماژول جدید آنرا به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
>ng g m Authentication -m app.module --routing
create src/app/authentication/authentication-routing.module.ts (257 bytes) create src/app/authentication/authentication.module.ts (311 bytes) update src/app/app.module.ts (696 bytes)
import { EmployeeRoutingModule } from './employee/employee-routing.module'; @NgModule({ imports: [ BrowserModule, AppRoutingModule, AuthenticationModule ]
بنابراین فایل app.module.ts چنین تعاریفی را پیدا میکند:
import { EmployeeModule } from './employee/employee.module'; @NgModule({ imports: [ BrowserModule, AuthenticationModule, AppRoutingModule ]
در ادامه کامپوننت جدید لاگین را به این ماژول اضافه میکنیم:
>ng g c Authentication/Login
create src/app/Authentication/login/login.component.html (24 bytes) create src/app/Authentication/login/login.component.ts (265 bytes) create src/app/Authentication/login/login.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (383 bytes)
import { LoginComponent } from "./login/login.component"; @NgModule({ declarations: [LoginComponent] })
در ادامه میخواهیم قالب این کامپوننت را در منوی اصلی سایت قابل دسترسی کنیم. به همین جهت به فایل src/app/authentication/authentication-routing.module.ts مراجعه کرده و مسیریابی این کامپوننت را تعریف میکنیم:
import { LoginComponent } from "./login/login.component"; const routes: Routes = [ { path: "login", component: LoginComponent } ];
ایجاد ماژولهای Core و Shared
در مطلب «سازماندهی برنامههای Angular توسط ماژولها» در مورد اهمیت ایجاد ماژولهای Core و Shared بحث شد. در اینجا نیز این دو ماژول را ایجاد خواهیم کرد.
فایل src\app\core\core.module.ts، جهت به اشتراک گذاری سرویسهای singleton سراسری برنامه، یک چنین ساختاری را پیدا میکند:
import { NgModule, SkipSelf, Optional, } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterModule } from "@angular/router"; // import RxJs needed operators only once import "./services/rxjs-operators"; import { BrowserStorageService } from "./browser-storage.service"; @NgModule({ imports: [CommonModule, RouterModule], exports: [ // components that are used in app.component.ts will be listed here. ], declarations: [ // components that are used in app.component.ts will be listed here. ], providers: [ // global singleton services of the whole app will be listed here. BrowserStorageService ] }) export class CoreModule { constructor( @Optional() @SkipSelf() core: CoreModule) { if (core) { throw new Error("CoreModule should be imported ONLY in AppModule."); } } }
همچنین سطر "import "./services/rxjs-operators نیز از مطلب «روشهایی برای مدیریت بهتر عملگرهای RxJS در برنامههای Angular» کمک میگیرد تا مدام نیاز به import عملگرهای rxjs نباشد.
و ساختار فایل src\app\shared\shared.module.ts جهت به اشتراک گذاری کامپوننتهای مشترک بین تمام ماژولها، به صورت ذیل است:
import { NgModule, ModuleWithProviders } from "@angular/core"; import { CommonModule } from "@angular/common"; @NgModule({ imports: [ CommonModule ], entryComponents: [ // All components about to be loaded "dynamically" need to be declared in the entryComponents section. ], declarations: [ // common and shared components/directives/pipes between more than one module and components will be listed here. ], exports: [ // common and shared components/directives/pipes between more than one module and components will be listed here. CommonModule ] /* No providers here! Since they’ll be already provided in AppModule. */ }) export class SharedModule { static forRoot(): ModuleWithProviders { // Forcing the whole app to use the returned providers from the AppModule only. return { ngModule: SharedModule, providers: [ /* All of your services here. It will hold the services needed by `itself`. */] }; } }
و در آخر تعاریف این دو ماژول جدید به فایل src\app\app.module.ts اضافه خواهند شد:
import { FormsModule } from "@angular/forms"; import { HttpClientModule } from "@angular/common/http"; import { CoreModule } from "./core/core.module"; import { SharedModule } from "./shared/shared.module"; @NgModule({ imports: [ BrowserModule, FormsModule, HttpClientModule, CoreModule, SharedModule.forRoot(), AuthenticationModule, AppRoutingModule ] }) export class AppModule { }
افزودن کامپوننت Header
در ادامه میخواهیم لینکی را به این مسیریابی جدید در نوار راهبری بالای سایت اضافه کنیم. همچنین قصد نداریم فایل app.component.html را با تعاریف آن شلوغ کنیم. به همین جهت یک کامپوننت هدر جدید را برای این منظور اضافه میکنیم:
> ng g c Header
create src/app/header/header.component.html (25 bytes) create src/app/header/header.component.ts (269 bytes) create src/app/header/header.component.css (0 bytes) update src/app/app.module.ts (1069 bytes)
<nav> <div> <div> <a [routerLink]="['/']">{{title}}</a> </div> <ul> <li role="menuitem" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a [routerLink]="['/welcome']">Home</a> </li> <li role="menuitem" routerLinkActive="active"> <a queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> </ul> </div> </nav>
export class HeaderComponent implements OnInit { title = "Angular.Jwt.Core";
در آخر به فایل app.component.html مراجعه کرده و selector این کامپوننت را در آن درج میکنیم:
<app-header></app-header> <div> <router-outlet></router-outlet> </div>
تا اینجا اگر دستور ng serve -o را صادر کنیم (کار build درون حافظهای، جهت محیط توسعه و نمایش خودکار برنامه در مرورگر)، چنین خروجی در مرورگر نمایان خواهد شد (البته میتوان پنجرهی کنسول ng serve را باز نگه داشت تا کار watch را به صورت خودکار انجام دهد؛ این روش سریعتر و به همراه build تدریجی است):
انتقال کامپوننتهایی که در app.component.ts استفاده میشوند به CoreModule
با توجه به مطلب «سازماندهی برنامههای Angular توسط ماژولها»، کامپوننتهایی که در app.component.ts مورد استفاده قرار میگیرند، باید به Core Module منتقل شوند و قسمت declarations فایل app.module.ts از آنها خالی گردد. به همین جهت پوشهی جدید src\app\core\component را ایجاد کرده و سپس پوشهی src\app\header را به آنجا منتقل میکنیم (با تمام فایلهای درون آن).
پس از آن، تعریف HeaderComponent را از قسمت declarations مربوط به AppModule حذف کرده و آنرا به دو قسمت exports و declarations مربوط به CoreModule منتقل میکنیم:
import { HeaderComponent } from "./component/header/header.component"; @NgModule({ exports: [ // components that are used in app.component.ts will be listed here. HeaderComponent ], declarations: [ // components that are used in app.component.ts will be listed here. HeaderComponent ] }) export class CoreModule {
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.