نظرات مطالب
آغاز به کار با Twitter Bootstrap در ASP.NET MVC
فایل bootstrap.css  بصورت ناقص فشرده می‌شود.
بطور دقیق‌تر تا رسیدن به کاراکترهای :: بدون مشکل کار پیش می‌رود.
در حالت اجرا وقتی فایل را css را باز میکنیم فقط کلاس‌ها و شناسه‌های قبل از :: وجود دارند. 
نظرات مطالب
معرفی فریم ورک Blueprint CSS
طرز کار همه css framework‌ها شبیه به هم هست . من از 960gs استفاده میکنم . تنها تفاوت این اضافه داشتن typography و style فرم هست که من خودم اضافه میکردم
مطالب
ASP.NET MVC #19

مروری بر امکانات Caching اطلاعات در ASP.NET MVC

در برنامه‌های وب، بالاترین حد کارآیی برنامه‌ها از طریق بهینه سازی الگوریتم‌ها حاصل نمی‌شود، بلکه با بکارگیری امکانات Caching سبب خواهیم شد تا اصلا کدی اجرا نشود. در ASP.NET MVC این هدف از طریق بکارگیری فیلتری به نام OutputCache میسر می‌گردد:

using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
}
}

همانطور که ملاحظه می‌کنید، OutputCache را به یک اکشن متد یا حتی به یک کنترلر نیز می‌توان اعمال کرد. به این ترتیب HTML نهایی حاصل از View متناظر با اکشن متد جاری فراخوانی شده، Cache خواهد شد. سپس زمانیکه درخواست بعدی به سرور ارسال می‌شود، نتیجه دریافت شده، همان اطلاعات Cache شده قبلی است و عملا در سمت سرور کدی اجرا نخواهد شد. در اینجا توسط پارامتر Duration، مدت زمان معتبر بودن کش حاصل، برحسب ثانیه مشخص می‌شود. VaryByParam مشخص می‌کند که اگر متدی پارامتری را دریافت می‌کند، آیا باید به ازای هر مقدار دریافتی، مقادیر کش شده متفاوتی ذخیره شوند یا خیر. در اینجا چون متد Index پارامتری ندارد، از مقدار none استفاده شده است.


مثال یک
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس کنترلر جدید Home را نیز به آن اضافه نمائید:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}

همچنین کدهای View متد Index را نیز به نحو زیر تغییر دهید:

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

در اینجا نمایش دو زمان دریافتی از کنترلر و زمان محاسبه شده در View را مشاهده می‌کنید. هدف این است که بررسی کنیم آیا فیلتر OutputCache بر روی این دو مقدار تاثیری دارد یا خیر.
برنامه را اجرا نمائید. سپس چند بار صفحه را Refresh کنید. مشاهده خواهید کرد که هر دو زمان یاد شده تا 60 ثانیه، تغییری نخواهند کرد و حاصل نهایی از Cache خواهنده می‌شود.
کاربرد یک چنین حالتی برای مثال نمایش اطلاعات بازدیدهای یک سایت است. نباید به ازای هر کاربر وارد شده به سایت، یکبار به بانک اطلاعاتی مراجعه کرد و آمار جدیدی را تهیه نمود. یا برای نمونه اگر جایی قرار است اطلاعات وضعیت آب و هوا نمایش داده شود، بهتر است این اطلاعات، مثلا هر نیم ساعت یکبار به روز شود و نه به ازای هر بازدید جدید از سایت، توسط صدها بازدید کننده همزمان. یا برای مثال کش کردن خروجی فید RSS یک بلاگ به مدت چند ساعت نیز ایده خوبی است. از این لحاظ که اگر اطلاعات بلاگ شما روزی یکبار به روز می‌شود، نیازی نیست تا به ازای هر برنامه فیدخوان، یکبار اطلاعات از بانک اطلاعاتی دریافت شده و پروسه رندر نهایی فید صورت گیرد. منوهای پویای یک سایت نیز در همین رده قرار می‌گیرند. دریافت اطلاعات منوهای پویای سایت به ازای هر درخواست رسیده کاربری جدید، کار اشتباهی است. این اطلاعات نیز باید کش شوند تا بار سرور کاهش یابد. البته تمام این‌ها زمانی میسر خواهند شد که اطلاعات سمت سرور کش شوند.


مثال دو
همان مثال قبلی را در اینجا جهت بررسی پارامتر VaryByParam به نحو زیر تغییر می‌دهیم:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index(string parameter)
{
ViewBag.Msg = parameter ?? string.Empty;
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}


در اینجا یک پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بین تگ‌های h2 نمایش داده خواهد شد. همچنین یک فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:

@{
ViewBag.Title = "Index";
}

<h2>@ViewBag.Msg</h2>

<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

@using (Html.BeginForm())
{
@Html.TextBox("parameter")
<input type="submit" />
}

اکنون برنامه را اجرا کنید. در TextBox نمایش داده شده یکبار مثلا بنویسید Test1 و فرم را به سرور ارسال نمائید. سپس مقدار Test2 را وارد کرده و ارسال نمائید. در بار دوم، خروجی صفحه همانند زمانی است که مقدار Test1 ارسال شده است. علت این است که مقدار VaryByParam به none تنظیم شده است و صرفنظر از ورودی کاربر، همان اطلاعات کش شده قبلی بازگشت داده خواهد شد. برای رفع این مشکل، متد Index را به نحو زیر تغییر دهید، به طوریکه مقدار VaryByParam به نام پارامتر متد جاری اشاره کند:

[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)

در ادامه مجددا برنامه را اجرا کنید. اکنون یکبار مقدار Test1 را به سرور ارسال کنید. سپس مقدار Test2 را ارسال نمائید. مجددا همین دو مرحله را با مقادیر Test1 و Test2 تکرار کنید. مشاهده خواهید کرد که اینبار اطلاعات بر اساس مقدار پارامتر ارسالی کش شده است.



تنظیمات متفاوت OutputCache

الف) VaryByParam : اگر مساوی none قرار گیرد، همواره همان مقدار کش شده قبلی نمایش داده می‌شود. اگر مقدار آن به نام پارامتر خاصی تنظیم شود، اطلاعات کش شده بر اساس مقادیر متفاوت پارامتر دریافتی، متفاوت خواهند بود. در اینجا پارامترهای متفاوت را با یک «,» می‌توان از هم جدا ساخت. اگر تعداد پارامترها زیاد است می‌توان مقدار VaryByParam را مساوی با * قرار داد. در این حالت به ازای مقادیر متفاوت دریافتی پارامترهای مختلف، اطلاعات مجزایی در کش قرار خواهد گرفت. این روش آخر آنچنان توصیه نمی‌شود چون سربار بالایی دارد و حجم بالایی از اطلاعات بر اساس پارامترهای مختلف، باید در کش قرار گیرند.
ب) Location : مکان قرارگیری اطلاعات کش شده را مشخص می‌کند. مقدار آن نیز بر اساس یک enum به نام OutputCacheLocation مشخص می‌گردد. در این حالت برای مثال می‌توان مکان‌های Server، Client و ServerAndClient را مقدار دهی نمود. مقدار Downstream به معنای کش شدن اطلاعات بر روی پروکسی سرورهای بین راه و یا مرورگرها است. پیش فرض آن Any است که ترکیبی از Server و Downstream می‌باشد.
اگر قرار است اطلاعات یکسانی به تمام کاربران نمایش داده شود، مثلا محتوای لیست یک منوی پویا،‌ محل قرارگیری اطلاعات کش باید سمت سرور باشد. اگر نیاز است به ازای هر کاربر محتوای اطلاعات کش شده متفاوت باشد، بهتر است محل سمت کلاینت را مقدار دهی نمود.
ج) VaryByHeader : اطلاعات، بر اساس هدرهای مشخص شده، کش می‌شوند. برای مثال مرسوم است که از Accept-Language در اینجا استفاده شود تا اطلاعات مثلا فرانسوی کش شده، به کاربر آلمانی تحویل داده نشود.
د) VaryByCustom :‌ در این حالت نام یک متد استاتیک تعریف شده در فایل global.asax.cs باید مشخص گردد. توسط این متد کلید رشته‌ای اطلاعاتی که قرار است کش شود، بازگشت داده خواهد شد.
ه) SqlDependency : در این حالت اطلاعات تا زمانیکه تغییری در جداول بانک اطلاعاتی SQL Server صورت نگیرد، کش خواهد شد.
و) Nostore : به پروکسی سرورهای بین راه و همچنین مرورگرها اطلاع می‌دهد که اطلاعات را نباید کش کنند. اگر قسمت اعتبار سنجی این سری را به خاطر داشته باشید، چنین تعریفی در قسمت Remote validation بکارگرفته شد:

[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]  

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

using System;
using System.Web.Mvc;

namespace MvcApplication16.Helper
{
/// <summary>
/// Adds "Cache-Control: private, max-age=0" header,
/// ensuring that the responses are not cached by the user's browser.
/// </summary>
public class NoCachingAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
filterContext.HttpContext.Response.CacheControl = "private";
filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
}
}
}

کار این فیلتر اضافه کردن هدر «Cache-Control: private, max-age=0» به Response است.


استفاده از فایل Web.Config برای معرفی تنظیمات Caching

یکی دیگر از تنظیمات ویژگی OutputCache، پارامتر CacheProfile است که امکان تنظیم آن در فایل web.config نیز وجود دارد. برای نمونه تنظیمات زیر را به قسمت system.web فایل وب کانفیگ برنامه اضافه کنید:


<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Aggressive" location="ServerAndClient" duration="300"/>
<add name="Mild" duration="100" location="Server" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>

سپس مثلا برای استفاده از پروفایلی به نام Aggressive، خواهیم داشت:

[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)


استفاده از ویژگی به نام donut caching

تا اینجا به این نتیجه رسیدیم که OutputCache، کل خروجی یک View را بر اساس پارامترهای مختلفی که دریافت می‌کند، کش خواهد کرد. در این بین اگر بخواهیم تنها قسمت کوچکی از صفحه کش نشود چه باید کرد؟ برای حل این مشکل قابلیتی به نام cache substitution که به donut caching هم معروف است (چون آن‌را می‌توان به شکل یک donut تصور کرد!) در ASP.NET MVC قابل استفاده است.

@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }

همانطور که ملاحظه می‌کنید برای تعریف یک چنین اطلاعاتی باید از متد Response.WriteSubstitution در یک view استفاده کرد. در این مثال، نمایش زمان جاری معرفی شده، صرف نظر از وضعیت کش صفحه جاری، کش نخواهد شد.

عکس آن هم ممکن است. فرض کنید که صفحه جاری شما از سه partial view تشکیل شده است. هر کدام از این partial viewها نیز مزین به OutpuCache هستند. اما صفحه اصلی درج کننده اطلاعات این سه partial view فاقد ویژگی Output کش است. در این حالت تنها اطلاعات این partial viewها کش خواهند شد و سایر قسمت‌های صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جدید به روز خواهند شد. حالت توصیه شده نیز همین مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومی درنظر داشته باشید.


استفاده از امکانات Data Caching به صورت مستقیم

مطالبی که تا اینجا عنوان شدند به کش کردن اطلاعات Response اختصاص داشتند. اما امکانات Caching موجود، به این مورد خلاصه نشده و می‌توان اطلاعات و اشیاء را نیز کش کرد. برای مثال اطلاعات «با سطح دسترسی عمومی» دریافتی از بانک اطلاعاتی توسط یک کوئری را نیز می‌توان کش کرد. جهت انجام اینکار می‌توان از متدهای HttpRuntime.Cache.Insert و یا HttpContext.Cache.Insert استفاده کرد. استفاده از HttpContext.Cache.Insert حین نوشتن Unit tests دردسر کمتری دارد و mocking آن ساده است؛ از این جهت که بر اساس HttpContextBase تعریف شده‌است.
در ادامه یک کلاس کمکی نوشتن اطلاعات در cache و سپس بازیابی آن‌را ملاحظه می‌کنید:

using System;
using System.Web;
using System.Web.Caching;

namespace MvcApplication16.Helper
{
public static class CacheManager
{
public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
{
if (data == null) return;
httpContext.Cache.Add(
key,
data,
null,
DateTime.Now.AddMinutes(durationMinutes),
TimeSpan.Zero,
CacheItemPriority.AboveNormal,
null);
}

public static T CacheRead<T>(this HttpContextBase httpContext, string key)
{
var data = httpContext.Cache[key];
if (data != null)
return (T)data;
return default(T);
}

public static void InvalidateCache(this HttpContextBase httpContext, string key)
{
httpContext.Cache.Remove(key);
}
}
}

و برای استفاده از آن در یک اکشن متد، ابتدا نیاز است فضای نام این کلاس تعریف شود و سپس برای نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext یکی از خواص تعریف شده در شیء کنترلر است که با ارث بری کنترلرها از آن، همواره در دسترس می‌باشد.
در اینجا برای نمونه اطلاعات یک لیست جنریک دریافتی از بانک اطلاعاتی را مثلا 10 دقیقه (بسته به پارامتر durationMinutes آن) می‌توان کش کرد و سپس توسط متد CacheRead آن‌را دریافت نمود. اگر متد CacheRead نال برگرداند به معنای خالی بودن کش است. بنابراین یکبار اطلاعات را از بانک اطلاعاتی دریافت نموده و سپس آن‌را کش خواهیم کردیم.
البته هستند ORMهایی که یک چنین کارهایی را به صورت توکار پشتیبانی کنند. به مکانیزم آن، Second level cache هم گفته می‌شود؛ به علاوه امکان استفاده از پروایدرهای دیگری را بجز کش IIS برای ذخیره سازی موقتی اطلاعات نیز فراهم می‌کنند.
همچنین باید دقت داشت این اعداد مدت زمان، هیچگونه ضمانتی ندارند. اگر IIS احساس کند که با کمبود منابع مواجه شده است، به سادگی شروع به حذف اطلاعات موجود در کش خواهد کرد.


نکته امنیتی مهم!
به هیچ عنوان از OutputCache در صفحاتی که نیاز به اعتبار سنجی دارند، استفاده نکنید و به همین جهت در قسمت کش کردن اطلاعات، بر روی «اطلاعاتی با سطح دسترسی عمومی» تاکید شد.
فرض کنید کارمندی به صفحه مشاهده فیش حقوقی خودش مراجعه کرده است. این ماه هم اضافه حقوق آنچنانی داشته است. شما هم این صفحه را به مدت سه ساعت کش کرده‌اید. آیا می‌توانید تصور کنید اگر همین گزارش کش شده با این اطلاعات، به سایر کارمندان نمایش داده شود چه قشقرقی به پا خواهد شد؟!
بنابراین هیچگاه اطلاعات مخصوص به یک کاربر اعتبار سنجی شده را کش نکنید و «تنها» اطلاعاتی نیاز به کش شدن دارند که عمومی باشند. برای مثال لیست آخرین اخبار سایت؛ لیست آخرین مدخل‌های فید RSS سایت؛ لیست اطلاعات منوی عمومی سایت؛ لیست تعداد کاربران مراجعه کننده به سایت در طول یک روز؛ گزارش آب و هوا و کلیه اطلاعاتی با سطح دسترسی عمومی که کش شدن آن‌ها مشکل ساز نباشد.
به صورت خلاصه هیچگاه در کدهای شما چنین تعریفی نباید مشاهده شود:
[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()




مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور
تا اینجا تنظیمات اصلی فرم ثبت اطلاعات کارمندان را انجام دادیم. اکنون نوبت به ارسال این اطلاعات به سمت سرور است. پیشنیاز آن نیز تدارک مواردی است که در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» پیشتر بحث شدند. از این مطلب تنها تنظیمات موارد ذیل را نیاز خواهیم داشت و از تکرار آن‌ها در اینجا صرفنظر می‌شود تا هم مطلب کوتاه‌تر شود و هم بتوان بر روی اصل موضوع جاری، تمرکز کرد:
- ایجاد یک پروژه‌ی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامه‌ی ASP.NET Core خالی برای اجرای یک برنامه‌ی Angular CLI
- تنظیمات فایل آغازین یک برنامه‌ی ASP.NET Core جهت ارائه‌ی برنامه‌های Angular
- ایجاد ساختار اولیه‌ی برنامه‌ی Angular CLI در داخل پروژه‌ی جاری: این مورد را تاکنون انجام داده‌ایم و تکمیل کرده‌ایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشه‌ی angular-template-driven-forms-lab (پروژه‌ی این سری) به ریشه‌ی پروژه‌ی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشه‌ی wwwroot
- روش اول و یا دوم اجرای برنامه‌های مبتنی بر ASP.NET Core و Angular CLI

البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز می‌توانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامه‌ی Angular و ASP.NET Core را می‌توان توسط VSCode به خوبی مدیریت و اجرا کرد.


ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور

در برنامه‌های Angular مرسوم است جهت کاهش مسئولیت‌های یک کلاس و امکان استفاده‌ی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاس‌های کامپوننت‌ها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آن‌را انجام می‌دهیم.
ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا می‌کنیم:
 >ng g s employee/FormPoster -m employee.module
با این خروجی
 installing service
  create src\app\employee\form-poster.service.spec.ts
  create src\app\employee\form-poster.service.ts
  update src\app\employee\employee.module.ts
همانطور که در سطر آخر نیز ملاحظه می‌کنید، فایل employee.module.ts را جهت درج کلاس جدید FormPosterService در قسمت providers ماژول آن به روز رسانی می‌کند؛ تا بتوانیم این سرویس را در کامپوننت‌های این ماژول تزریق کرده و استفاده کنیم.
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر می‌دهیم:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

import { Employee } from './employee';

@Injectable()
export class FormPosterService {

    constructor(private http:Http) {
    }

    postEmployeeForm(employee: Employee) {
    }
}
در اینجا سرویس Http انگیولار به سازنده‌ی کلاس تزریق شده‌است و این نحوه‌ی تعریف سبب می‌شود تا بتوان به پارامتر http، به صورت یک فیلد خصوصی تعریف شده‌ی در سطح کلاس نیز دسترسی پیدا کنیم.
چون این کلاس از ماژول توکار Http استفاده می‌کند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
import { HttpModule } from "@angular/http";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    EmployeeModule,
    AppRoutingModule
  ]
اکنون می‌توانیم این سرویس جدید FormPosterService را به سازنده‌ی کامپوننت EmployeeRegisterComponent در فایل src\app\employee\employee-register\employee-register.component.ts تزریق کنیم:
import { FormPosterService } from "../form-poster.service";

export class EmployeeRegisterComponent implements OnInit {

  constructor(private formPoster: FormPosterService) {}

}

در ادامه برای آزمایش برنامه، به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی، دستورات:
>npm install
>ng build --watch
و در دومی، دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
دستورات اول کار بازیابی وابستگی‌های سمت کلاینت و سپس ساخت تدریجی برنامه‌ی Angular را دنبال می‌کند. دستورات دوم، وابستگی‌های برنامه‌ی ASP.NET Core را دریافت و نصب کرده و سپس برنامه را در حالت watch ساخته و بر روی پورت 5000 ارائه می‌کند (بدون نیاز به اجرای VS 2017؛ این دستور عمومی است).
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگه‌ی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.


مدیریت رخداد submit فرم در Angular

تا اینجا کار برپایی تنظیمات اولیه‌ی کار با سرویس Http را انجام دادیم. مرحله‌ی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
در حین رخدادگردانی submit می‌توان به template reference variable تعریف شده‌ی form# برای دسترسی به وهله‌ای از ngForm نیز کمک گرفت.
export class EmployeeRegisterComponent implements OnInit {
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
امضای متد submitForm را در اینجا مشاهده می‌کنید. form دریافتی آن از نوع NgForm است که در ابتدای فایل import شده‌است.
در همین حال اگر بر روی دکمه‌ی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر می‌توان مشاهده کرد:


اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کرده‌است. همانطور که مشاهده می‌کنید، مقدار form.value بسیار شبیه است به وهله‌ای از مدلی که در سطح کلاس تعریف کرده‌ایم و این مقدار همواره توسط Angular نگهداری و مدیریت می‌شود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرم‌های مبتنی بر قالب‌ها به صورت جداگانه‌ای تهیه کرد. توسط شیء form نیز می‌توان به تمام اطلاعات فیلدها دسترسی یافت.


تکمیل سرویس ارسال اطلاعات به سرور

در ادامه می‌خواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { Http, Response, Headers, RequestOptions } from "@angular/http";

import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Employee } from "./employee";

@Injectable()
export class FormPosterService {
  private baseUrl = "api/employee";

  constructor(private http: Http) {}

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

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

  postEmployeeForm(employee: Employee): Observable<Employee> {
    const body = JSON.stringify(employee);
    const headers = new Headers({ "Content-Type": "application/json" });
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(this.baseUrl, body, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
برای کار با Observables یا می‌توان نوشت 'import 'rxjs/Rx که تمام بسته‌ی RxJS را import می‌کند، یا همانند این مثال بهتر است تنها اپراتورهایی را که به آن‌ها نیاز پیدا می‌کنیم، import نمائیم. به این ترتیب حجم نهایی ارائه‌ی برنامه نیز کاهش خواهد یافت.
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode می‌شود. البته متد post اینکار را به صورت توکار نیز می‌تواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمی‌داند که چه نوع فرمتی را دریافت کرده‌است و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده می‌شود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل می‌کنیم:
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);

    // validate form
    this.validatePrimaryLanguage(this.model.primaryLanguage);
    if (this.hasPrimaryLanguageError) {
      return;
    }

    this.formPoster
      .postEmployeeForm(this.model)
      .subscribe(
        data => console.log("success: ", data),
        err => console.log("error: ", err)
      );
  }
در اینجا ابتدا اعتبارسنجی سفارشی drop down را که در قسمت قبل بررسی کردیم، قرار داده‌ایم. پس از آن متد postEmployeeForm سرویس formPoster فراخوانی شده‌است و در اینجا کار subscribe به نتیجه‌ی عملیات صورت گرفته‌است که می‌تواند حاوی اطلاعاتی از سمت سرور و یا خطایی در این بین باشد.

یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.


تکمیل Web API برنامه‌ی ASP.NET Core جهت دریافت اطلاعات از کلاینت‌ها

در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
export class FormPosterService {
  private baseUrl = "api/employee";
به همین جهت نیاز است سرویس Web API سمت سرور خود را بر این مبنا تکمیل کنیم.
ابتدا مدل زیر را به پروژه‌ی ASP.NET Core جاری، معادل نمونه‌ی تایپ‌اسکریپتی سمت کلاینت آن اضافه می‌کنیم. البته در اینجا یک Id نیز اضافه شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool IsFullTime { get; set; }
        public string PaymentType { get; set; }
        public string PrimaryLanguage { get; set; }
    }
}

سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
using Microsoft.AspNetCore.Mvc;
using AngularTemplateDrivenFormsLab.Models;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class EmployeeController : Controller
    {
        public IActionResult Post([FromBody] Employee model)
        {
            //todo: save model

            model.Id = 100;
            return Created("", new { fields = model });
        }
    }
}
این کنترلر با شیوه‌ی Web API تعریف شده‌است. مسیریابی آن با api شروع می‌شود تا با مسیر baseUrl سرویس formPoster تطابق پیدا کند.
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه می‌کنید، بازگشت داده شده‌است. این نوع خروجی، یک چنین JSON ایی را تولید می‌کند:
{"fields":{"id":100,"firstName":"Vahid","lastName":"N","isFullTime":true,"paymentType":"FullTime","primaryLanguage":"Persian"}}
به همین جهت است که در متد extractData، دسترسی به body.fields را مشاهده می‌کنید. این fields در اینجا دربرگیرنده‌ی اطلاعات بازگشتی از سرور است (نام آن دلخواه است و درصورت تغییر آن در سمت سرو، باید این نام را در متد extractData نیز اصلاح کنید).
  private extractData(res: Response) {
    const body = res.json();
    return body.fields || {};
  }
اکنون اگر برنامه را با دستورات dotnet watch build و ng build --watch اجرا کنیم، بر روی پورت 5000 قابل دسترسی خواهد بود و پس از ارسال فرم به سرور، چنین خروجی را می‌توان در کنسول developer مرورگر مشاهده کرد:


نمایش success به همراه شیءایی که از سمت سرور دریافت شده‌است؛ که حاصل اجرای سطر ذیل در متد submitForm است:
 data => console.log("success: ", data)
همانطور که مشاهده می‌کنید، این شیء به همراه Id نیز هست. بنابراین درصورت نیاز به آن در سمت کلاینت، خاصیت معادل آن‌را به کلاس کارمند اضافه کرده و در همین سطر فوق می‌توان به آن دسترسی یافت.


بارگذاری اطلاعات drop down از سرور

تا اینجا اطلاعات drop down نمایش داده شده از یک آرایه‌ی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آن‌ها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
[HttpGet("/api/[controller]/[action]")]
public IActionResult Languages()
{
    string[] languages = { "Persian", "English", "Spanish", "Other" };
    return Ok(languages);
}
که برای آزمایش آن می‌توانید مسیر http://localhost:5000/api/employee/languages را جداگانه در مرورگر درخواست کنید.
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه می‌کنیم که کار آن‌ها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
  private extractLanguages(res: Response) {
    const body = res.json();
    return body || {};
  }

  getLanguages(): Observable<any> {
    return this.http
      .get(`${this.baseUrl}/languages`)
      .map(this.extractLanguages)
      .catch(this.handleError);
  }
اینبار چون خروجی سمت سرور را مانند قبل (متد extractData) داخل فیلدی مانند fields محصور نکردیم، همان body دریافتی بازگشت داده شده‌است.
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
  languages = [];

  ngOnInit() {
    this.formPoster
      .getLanguages()
      .subscribe(
        data => this.languages = data,
        err => console.log("get error: ", err)
      );
  }
ابتدا آرایه‌ی زبان‌ها با یک آرایه‌ی خالی مقدار دهی شده‌است و سپس در متد ngOnInit، کار دریافت اطلاعات آن از سرور، صورت گرفته‌است.

مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت می‌توان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
<h3 *ngIf="languages.length == 0">Loading...</h3>
<div class="container" *ngIf="languages.length > 0">
در این حالت هر زمانیکه آرایه‌ی زبان‌ها پر شد، loading حذف شده و div نمایان می‌گردد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات:
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
نظرات مطالب
یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017
- این مورد انتخاب شخصی است بیشتر. اگر می‌خواهید یکی را با VSCode کار کنید و دیگری را با VS کامل، شاید جدا باشند بهتر باشد. یا اینکه هر دو را هم می‌توان با VSCode کار کرد (اگر NET Core. کار می‌کنید).
- بستگی به توزیع نهایی برنامه دارد. آیا قرار است برنامه‌ی Angular بر روی یک پورت دیگر و یا یک دومین دیگر به صورت مجزایی ارائه شود؟ بله. در این‌حالت باید CORS فعال شود (یک مثال فعالسازی آن). در غیراینصورت اگر فایل‌های نهایی Angular در پوشه‌ی wwwroot برنامه‌ی وب کپی می‌شوند، نیازی به تغییر اضافه‌تری نیست.
مطالب
ماندگاری با تاخیر در SQL Server 2014
به صورت پیش فرض SQL Server از روش  write-ahead log - WAL استفاده می‌کند. به این معنا که کلیه تغییرات، پیش از commit نهایی باید در لاگ فایل آن نوشته شوند. این مساله با تعداد بالای تراکنش‌ها تا حدودی بر روی سرعت سیستم می‌تواند تاثیرگذار باشد. برای بهبود این وضعیت، در SQL Server 2014 قابلیتی به نام delayed_durability اضافه شده‌است که با فعال سازی آن، کلیه اعمال مرتبط با لاگ‌های تراکنش‌ها به صورت غیرهمزمان انجام می‌شوند. به این ترتیب تراکنش‌ها زودتر از معمول به پایان خواهد رسید؛ با این فرض که نوشته شدن تغییرات در لاگ فایل‌ها، در آینده‌ای محتمل انجام خواهند شد. این مساله به معنای فدا کردن D در ACID است (Atomicity, Consistency, Isolation, Durability). البته باید دقت داشت که رسیدن به ACID کامل هزینه‌بر است و شاید خیلی از اوقات تمام اجزای آن نیازی نباشند یا حتی بتوان با اندکی تخفیف آن‌ها را اعمال کرد؛ مانند D به تاخیر افتاده.
برای اینکار SQL Server از یک بافر 60 کیلوبایتی برای ذخیره سازی اطلاعات لاگ‌هایی که قرار است به صورت غیرهمزمان با تراکنش‌ها نوشته شوند، استفاده می‌کند. هر زمان که این 60KB پر شد، آن‌را flush کرده و ثبت خواهد نمود. به این ترتیب به دو مزیت خواهیم رسید:
- پردازش تراکنش‌ها بدون منتظر شدن جهت commit نهایی در دیسک سخت ادامه خواهند یافت. صبر کمتر به معنای امکان پردازش تراکنش‌های بیشتری در یک سیستم پر ترافیک است.
- با توجه به بافری که از آن صحبت شد، اینبار اعمال Write به صورت یک سری batch اعمال می‌شوند که کارآیی و سرعت بیشتری نسبت به حالت تکی دارند.

اندکی تاریخچه
ایده یک چنین عملی 28 سال قبل توسط Hal Berenson ارائه شده‌است! اوراکل آن‌را در سال 2006 تحت عنوان Asynchronous Commit پیاده سازی کرد و مایکروسافت در سال 2014 آن‌را ارائه داده‌است.


فعال سازی ماندگاری غیرهمزمان در SQL Server

فعال سازی این قابلیت در سطح بانک اطلاعاتی، در سطح یک تراکنش مشخص و یا در سطح رویه‌های ذخیره شده کامپایل شده مخصوص OLTP درون حافظه‌ای، میسر است.
برای فعال سازی ماندگاری با تاخیر در سطح یک دیتابیس، خواهیم داشت:
 ALTER DATABASE dbname SET DELAYED_DURABILITY = DISABLED | ALLOWED | FORCED;


در اینجا اگر ALLOWED را انتخاب کنید، به این معنا است که لاگ کلیه تراکنش‌های مرتبط با این بانک اطلاعاتی به صورت غیرهمزمان نوشته می‌شوند. حالت FORCED نیز دقیقا به همین معنا است با این تفاوت که اگر حالت ALLOWED انتخاب شود، تراکنش‌های ماندگار (آن‌هایی که به صورت دستی DELAYED_DURABILITY را غیرفعال کرده‌اند)، سبب flush کلیه تراکنش‌هایی با ماندگاری به تاخیر افتاده خواهند شد و سپس اجرا می‌شوند. در حالت Forced تنظیم دسترسی DELAYED_DURABILITY = OFF در سطح تراکنش‌ها تاثیری نخواهد داشت؛ اما در حالت ALLOWED این مساله به صورت دستی در سطح یک تراکنش قابل لغو است.
البته باید توجه داشت، صرفنظر از این تنظیمات، یک سری از تراکنش‌ها همیشه ماندگار هستند و بدون تاخیر؛ مانند تراکنش‌های سیستمی، تراکنش‌های بین دو یا چند بانک اطلاعاتی و کلیه تراکنش‌هایی که با FileTable، Change Data Capture و Change Tracking سر و کار دارند.

در سطح تراکنش‌های می‌توان نوشت:
 COMMIT TRANSACTION WITH (DELAYED_DURABILITY = ON);
و یا در رویه‌های ذخیره شده کامپایل شده مخصوص OLTP درون حافظه‌ای خواهیم داشت:
 BEGIN ATOMIC WITH (DELAYED_DURABILITY = ON, ...)

سؤال: آیا فعال سازی DELAYED_DURABILITY بر روی مباحث locking و isolation levels تاثیر دارند؟
پاسخ: خیر. کلیه تنظیمات قفل گذاری‌ها همانند قبل و بر اساس isolation levels تعیین شده، رخ خواهند داد. تنها تفاوت در اینجا است که با فعال سازی DELAYED_DURABILITY، کار commit بدون صبر کردن برای پایان نوشته شدن اطلاعات در لاگ سیستم صورت می‌گیرد. به این ترتیب قفل‌های انجام شده زودتر آزاد خواهند شد.

سؤال: میزان از دست دادن اطلاعات احتمالی در این روش چقدر است؟
در صورتیکه سرور کرش کند یا ری‌استارت شود، حداکثر به اندازه‌ی 60KB اطلاعات را از دست خواهید داد (اندازه‌ی بافری که برای اینکار درنظر گرفته شده‌است). البته عنوان شده‌است که اگر ری‌استارت یا خاموشی سرور، از پیش تعیین شده باشد، ابتدا کلیه لاگ‌های flush نشده، ذخیره شده و سپس ادامه‌ی کار صورت خواهد گرفت؛ ولی زیاد به آن اطمینان نکنید. اما همواره با فراخوانی sys.sp_flush_log، می‌توان به صورت دستی بافر لاگ‌های سیستم را flush کرد.


یک آزمایش

در ادامه قصد داریم یک جدول جدید را در بانک اطلاعاتی آزمایشی testdb2 ایجاد کنیم. سپس یکبار تنظیم DELAYED_DURABILITY = FORCED را انجام داده و 10 هزار رکورد را ثبت می‌کنیم و بار دیگر DELAYED_DURABILITY = DISABLED را تنظیم کرده و همین عملیات را تکرار خواهیم کرد:
CREATE TABLE tblData(
    ID INT IDENTITY(1, 1),
    Data1 VARCHAR(50),
    Data2 INT
);
CREATE CLUSTERED INDEX PK_tblData ON tblData(ID);
CREATE NONCLUSTERED INDEX IX_tblData_Data2 ON tblData(Data2);
 
-------------------------

alter database testdb2 SET DELAYED_DURABILITY = FORCED;

-------------------------

SET NOCOUNT ON
Print 'DELAYED_DURABILITY = FORCED'
DECLARE @counter AS INT = 0
DECLARE @start datetime = getdate()
WHILE (@counter < 10000)
BEGIN
      INSERT INTO tblData (Data1, Data2) VALUES('My Data', @counter)
      SET @counter += 1
END
Print DATEDIFF(ms,@start,getdate());
GO

-------------------------

alter database testdb2 SET DELAYED_DURABILITY = DISABLED;
truncate table tblData;
-------------------------

SET NOCOUNT ON
Print 'DELAYED_DURABILITY = DISABLED'
DECLARE @counter AS INT = 0
DECLARE @start datetime = getdate()
WHILE (@counter < 10000)
BEGIN
      INSERT INTO tblData (Data1, Data2) VALUES('My Data', @counter)
      SET @counter += 1
END
Print DATEDIFF(ms,@start,getdate());
GO

-----------------------
با این خروجی:
 DELAYED_DURABILITY = FORCED
666
DELAYED_DURABILITY = DISABLED
2883
در این آزمایش، سرعت insertها در حالت DELAYED_DURABILITY = FORCED حدود 4 برابر است نسبت به حالت معمولی.


برای مطالعه بیشتر

Control Transaction Durability
SQL Server 2014 Delayed Durability/Lazy Commit
Delayed Durability in SQL Server 2014 – Part 1
Is In-Memory OLTP Always a silver bullet for achieving better transactional speed
Delayed Durability in SQL Server 2014
مطالب
سشن صفر

یکی از تفاوت‌های مهم امنیتی ویندوزهای جدید (از سرویس پک 2 ویندوز 2003 به بعد)، اجرای سرویس‌های ویندوز در جلسه‌ای کاری (سشنی) جدا از جسله کاری برنامه‌های عادی است که توسط کاربران وارد شده به سیستم اجرا می‌شوند. در این ویندوزها، تنها سرویس‌ها هستند که در سشن صفر اجرا می‌شوند و مابقی برنامه‌های سایر کاربران در سشن‌های دیگر.



این برخلاف چیزی است که برای مثال در ویندوز XP وجود دارد؛ یعنی اجرای دوستانه‌ی تمام برنامه‌ها در یک سشن (چون کاربر وارد شده به سیستم نیز در سشن صفر قرار می‌گیرد).



در این حالت برنامه‌های عادی می‌توانند سرویس‌های ویندوز را به راحتی مورد حمله قرار دهند. اطلاعات بیشتر:


مهم‌ترین معنای اجرای ایزوله سرویس‌ها در سشن صفر، به خاطره سپرده شدن سرویس‌هایی است که امکان تعامل با کاربر را داشتند؛ برای مثال سرویس‌هایی که یک رابط کاربری را نمایش می‌دادند (به کمک همان تیک معروف Allow Service to Interact with Desktop در صفحه تنظیمات یک سرویس). زیرا اکنون اولین کاربر وارد شده به سیستم، در سشن یک قرار می‌گیرد (و نه همانند قبل در سشن صفر). به این صورت دیگر به رابط کاربری نمایش داده شده در سشن صفر دسترسی نداشته و چیزی را مشاهده نخواهد کرد.




البته شاید سؤال بپرسید که چه سرویس‌هایی نیاز به نمایش رابط کاربری دارند؟ چون عموما سرویسی که صحیح طراحی شده باشد نیاز به تعامل مستقیم با کاربر را از طریق رابط کاربری ندارد.
و پاسخ این است که خیلی از سرویس‌ها! در بسیاری از برنامه‌های متداول، امکان اجرای به صورت سرویس ویندوز NT پیش بینی نشده است. اگر نیاز باشد تا این نوع برنامه‌ها را به صورت سرویس، برای مثال در یک ویندوز سرور اجرا کرد (تا همیشه در حال اجرا باشند و همچنین با ری استارت شدن سیستم نیز مجددا بدون دخالت کاربر شروع به کار کنند)، در این حالت نیاز خواهد بود تا رابط کاربری آن‌ها نیز نمایش داده شوند.

سؤال: در ویندوزهای جدید برای تعامل با سشن صفر چه باید کرد؟
در ویندوز 2003 (به همراه تمام به روز رسانی‌ها) برای اتصال به سشن صفر می‌توان دستور زیر را در خط فرمان صادر کرد:
Mstsc /console

به این ترتیب امکان اتصال به سشن کنسول یا همان سشن صفر میسر می‌شود.
این پارامتر در ویندوز سرور 2008 ندید گرفته خواهد شد زیرا در این ویندوز امکان نصب یک سرویس در سشن‌هایی بجز سشن صفر نیز پیش بینی شده است؛ هر چند پیش فرض نصب همان سشن صفر است. اما به جهت سازگاری با نگارش‌های قبل این‌بار بجای پارامتر console از پارامتر admin استفاده کنید.

با استفاده از برنامه نویسی چطور؟
روش کار در این مقاله توضیح داده شده است اما سطر اول آن به این نکته تاکید دارد که ... این‌کار را نکنید! سرویسی که درست طراحی شده باشد نیازی به رابط کاربری تعاملی عموما جهت اعمال تنظیمات آن، ندارد.

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


کلاس‌های CSS اعتبارسنجی در Angular

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

  کلاس CSS اعتبارسنجی  توضیحات 
  ng-untouched   زمانیکه فرمی برای بار اول رندر می‌شود، تمام فیلدهای آن با کلاس CSS ایی به نام ng-untouched علامتگذاری می‌شوند. 
  ng-touched   همینقدر که کاربر با یک Tab از فیلدی عبور کند، با کلاس ng-touched مزین خواهد شد. بنابراین مهم نیست که حتما داده‌ای وارد شده باشد یا خیر. حتی عبور از یک فیلد نیز به معنای لمس آن نیز می‌باشد. 
 ng-pristine   مربوط به زمانی‌‌است که یک فیلد نه تغییر کرده‌است و نه لمس شده‌است. 
  ng-dirty   همینقدر که کاربر، تغییری را در فیلدی ایجاد کند، آن المان با کلاس ng-dirty مشخص خواهد شد. 
 ng-valid   برای حالت موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده می‌شود. 
  ng-invalid   برای حالت غیر موفقیت آمیز بودن اعتبارسنجی، به آن المان انتساب داده می‌شود. 

برای اینکه بتوانیم این موارد را در عمل مشاهده کنیم، به ابتدای فرم مثال این سری، تغییرات ذیل را اعمال خواهیم کرد:
<div class="form-group">
    <label>First Name</label>
    <input #firstName required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
</div>

<h3>Classes</h3>
<h4>{{ firstName.className }}</h4>
برای اینکه مشخص کنیم چه کلاسی به المان firstName متصل شده‌است، ابتدا نیاز است یک template reference variable را برای آن تعریف کنیم که اینکار را توسط معرفی firstName# انجام داده‌ایم. به این ترتیب است که می‌توان به خاصیت className آن در ادامه دسترسی یافت.


تصویر فوق کلاس‌هایی را نمایش می‌دهد که در اولین بار نمایش فرم، به المان firstName متصل شده‌اند. برای مثال در این حالت کلاس ng-pristine قابل مشاهده‌است و هنوز تغییری در آن حاصل نشده‌است.
در ادامه اگر حرفی را به آن اضافه کنیم:


هنوز هم ng-untouched آن برقرار است؛ اما ng-pristine آن به ng-dirty تبدیل شده‌است. در اینجا حتی اگر کل اطلاعات فیلد را نیز حذف کنیم و آن‌را خالی کنیم یا به حالت اول بازگردانیم نیز کلاس ng-dirty قابل مشاهده‌است. بنابراین اگر حالت فیلدی dirty شد، همواره به همین حالت باقی می‌ماند.
در این لحظه اگر با Tab به فیلد دیگری در فرم مراجعه کنیم:


در اینجا است که کلاس ng-untouched به ng-touched تبدیل می‌شود. بنابراین کلاس‌های مختلف لمس یک فیلد، ارتباطی به افزوده شدن یا حذف کاراکتری از یک فیلد ندارند و فقط به از دست رفتن focus و مراجعه‌ی به فیلد دیگری مرتبط می‌شوند.

اگر به المان تغییر یافته‌ی فوق دقت کنید، ویژگی required نیز به آن اضافه شده‌است (علاوه بر template reference variable ایی که تعریف کردیم). در این حالت کل فیلد را خالی کنید:


همانطور که مشاهده می‌کنید، اکنون کلاس ng-valid به کلاس ng-invalid تغییر یافته‌است.


ارتباط بین کلاس‌های CSS اعتبارسنجی و خواص ngModel

تمام کلاس‌های -ng ایی که در بالا معرفی شدند، معادل‌های خواص ngModel ایی نیز دارند. فقط کافی است -ng آن‌ها را حذف کنید، باقیمانده‌ی آن، نام خاصیت متناظری در ngModel خواهد بود. برای مثال کلاس ng-untouched به خاصیت untouched نگاشت می‌شود و به همین ترتیب برای مابقی.
template reference variable ایی را که تا به اینجا به المان اضافه کردیم (firstName#) به خواص همان المان دسترسی دارد (مانند className آن). اما در ادامه می‌خواهیم این متغیر به ngModel و خواص آن دسترسی داشته باشد و میدان دید آن تغییر کند. به همین جهت تنها کافی است تا ngModel را به این متغیر انتساب دهیم:
<div class="form-group">
    <label>First Name</label>
    <input #firstName="ngModel" required type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
</div>

<h4>dirty: {{ firstName.dirty }}</h4>
پس از این تغییر اکنون template reference variable تعریف شده می‌تواند برای نمونه به خاصیت dirty شیء ngModel دسترسی پیدا کند.


یک نکته: در حالت خواص valid و invalid که مرتبط با اعتبارسنجی هستند، خاصیت سومی نیز به نام errors وجود دارد که حاوی اطلاعات بیشتری در مورد خطای اعتبارسنجی رخ داده‌است. بنابراین وجود این خاصیت و نال نبودن آن نیز دلالت بر وجود یک خطای اعتبارسنجی است. از خاصیت errors در ادامه‌ی بحث در قسمت «مدیریت چندین خطای همزمان اعتبارسنجی» استفاده خواهیم کرد.


نمایش بهتر خطاهای اعتبارسنجی با بررسی خواص ngModel

یکی از مزایای کار با خواص ngModel، امکان استفاده‌ی از آن‌ها در عبارات شرطی‌است که نسبت به کلاس‌های CSS معرفی شده‌ی در ابتدای بحث، انعطاف پذیری بیشتری را به همراه خواهند داشت.
<div class="form-group">
    <label>First Name</label>
    <input #firstName="ngModel" #firstNameElement required type="text" class="form-control" 
              name="firstName" [(ngModel)]="model.firstName">
    <div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger">
        First Name is required.
    </div>      
</div>

<h4>className: {{ firstNameElement.className }}</h4>
<h4>dirty: {{ firstName.dirty }}</h4>
<h4>invalid: {{ firstName.invalid }}</h4>
توسط ngIf می‌توان المانی را به DOM اضافه و یا کلا از آن حذف کرد. در اینجا یک عبارت boolean به آن نسبت داده شده‌است. ابتدا حالت firstName.invalid را بررسی کنید. مشاهده خواهید کرد که اگر فرم برای بار اول و با مقادیر خالی نمایش داده شود، div خطا نیز ظاهر می‌شود که آنچنان خوشایند نیست و بهتر است خطاها را پس از اینکه کاربر مشغول به کار با فرم شد، به او نمایش دهیم؛ تا اینکه از همان ابتدا این خطاها به صورت واضحی نمایش داده شوند. بنابراین && firstName.touched نیز در اینجا اضافه شده‌است. به این ترتیب div نمایش دهنده‌ی alert بوت استرپ، دیگر در اولین بار نمایش یک فرم خالی، ظاهر نخواهد شد. اما اگر کاربر با یک tab از فیلدی خالی رد شد، آنگاه این خطا نمایش داده می‌شود.



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

علاوه بر نمایش یک alert بوت استرپی متناظر با یک فیلد غیرمعتبر، می‌توان خود المان‌های ورودی را نیز با شیوه‌نامه‌هایی مزین ساخت.


این کار را در بوت استرپ با افزودن کلاس has-error در کنار form-group انجام می‌دهند. همچنین label نیز باید به کلاس control-label مزین شود تا hass-error بر روی آن نیز تاثیرگذار شود. برای پیاده سازی پویای آن در Angular به روش ذیل عمل می‌شود:
<div class="form-group" [class.has-error]="firstName.invalid && firstName.touched">
   <label class="control-label">First Name</label>
در اینجا روش افزودن شرطی کلاس ویژه‌ی has-error بوت استرپ را به مجموعه کلاس‌های div جاری ملاحظه می‌کنید. هر زمان که شرط ذکر شده برقرار باشد، در عبارت property binding مخصوص class.className، این className را به صورت خودکار به مجموعه کلاس‌های آن المان اضافه می‌کند و برعکس.
بنابراین اگر المان firstName خالی باشد و همچنین با یک Tab از روی آن عبور کرده باشیم، کلاس has-error در کنار کلاس form-group اضافه می‌شود.

روش دوم: همانطور که در ابتدای بحث نیز عنوان شد، Angular بر اساس حالات مختلف یک فیلد، کلاس‌های CSS خاصی را به آن‌ها انتساب می‌دهد. یک چنین کاری را با مقدار دهی این کلاس‌ها در فایل src\styles.css نیز می‌توان انجام داد که دقیقا معادل بررسی خواص invalid و touched با کدنویسی است:
.ng-touched.ng-invalid{
    border: 1px solid red;
}


سایر ویژگی‌های اعتبارسنجی HTML 5

تا اینجا ویژگی استاندارد required را به المان ورودی فرم ثبت اطلاعات کاربران، اضافه کردیم.
<input #firstName="ngModel" #firstNameElement 
required maxlength="3" minlength="2" pattern="^V.*"
type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
در اینجا برای مثال اعمال ویژگی‌های maxlength، minlength و pattern را مشاهده می‌کنید. ویژگی pattern برای تعریف عبارات باقاعده بکار می‌رود (برای مثال، نام حتما باید با V شروع شود) و تقریبا در تمام مرورگرها (caniuse.com ) نیز پشتیبانی می‌شود.
برای نمونه minlength همه‌جا پشتیبانی نمی‌شود؛ اما آن‌را می‌توان برای مثال با الگویی مساوی "+..." جایگزین کرد.


مشکل! ذکر چند ویژگی اعتبارسنجی با هم، تداخل ایجاد می‌کنند!

در اینجا چون چهار ویژگی مختلف را به صورت یکجا به یک المان متصل کرده‌ایم، اکنون div ذیل به هر کدام از این ویژگی‌ها به صورت یکسانی واکنش نشان خواهد داد؛ زیرا خاصیت invalid را true می‌کنند:
    <div *ngIf="firstName.invalid && firstName.touched" class="alert alert-danger">
        First Name is required.
    </div>
روش مدیریت این حالت، به صورت ذیل است:
    <div class="form-group" [class.has-error]="firstName.invalid && firstName.touched">
      <label class="control-label">First Name</label>
      <input #firstName="ngModel" #firstNameElement 
             required maxlength="3" minlength="2" pattern="^V.*"
             type="text" class="form-control" name="firstName" [(ngModel)]="model.firstName">
      <div *ngIf="firstName.invalid && firstName.touched">
        <div class="alert alert-info">
           errors: {{ firstName.errors | json }}
        </div> 
        <div class="alert alert-danger" *ngIf="firstName.errors.required">
           Name is required.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.minlength">
           Name should be minimum {{firstName.errors.minlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.maxlength">
           Name should be max {{firstName.errors.maxlength.requiredLength}} characters.
        </div>
        <div class="alert alert-danger" *ngIf="firstName.errors.pattern">
           Name pattern: {{firstName.errors.pattern.requiredPattern}}
        </div>
      </div>      
    </div>
با یک چنین خروجی:


همانطور که در تصویر ملاحظه می‌کنید، محتوای خاصیت errors به صورت JSON، جهت دیباگ نیز درج شده‌است.
بنابراین وجود خاصیت firstName.errors.minlength و یا firstName.errors.pattern به این معنا است که این خطاهای خاص وجود دارند (خاصیت firstName.errors به همراه اضافاتی است) و برعکس نال بودن آن‌ها مؤید عدم وجود خطایی است. به همین جهت می‌توان به ازای هر کدام، یک div جداگانه را تشکیل داد.
مرحله‌ی بعد، استخراج اطلاعات بیشتری از همین شیء و محتوای خاصیت errors است. برای مثال زمانیکه در آن خاصیت minlength ظاهر شد، این خاصیت نیز دارای خاصیتی مانند requiredLength است که از آن می‌توان جهت درج عدد واقعی مورد نیاز این اعتبارسنج استفاده کرد.


بهبود اعتبارسنجی drop down

در حالت فعلی تعریف drop down مثال این سری، نیازی به اعتبارسنجی نیست؛ چون لیست مشخصی از طریق کامپوننت در اختیار این المان قرار می‌گیرد و همواره دقیقا یکی از این عناصر انتخاب خواهند شد. اما اگر گزینه‌ی دیگری را مانند:
<option value="default">Select a Language...</option>
به ابتدای این لیست اضافه کنیم، اینبار نیاز به اعتبارسنجی خواهد بود؛ زیرا اکنون کاربر می‌تواند گزینه‌ای را انتخاب نکند و راهی پیش فرض هم برای اعتبارسنجی این گزینه وجود ندارد.
    <div class="form-group" [class.has-error]="hasPrimaryLanguageError">
      <label class="control-label">Primary Language</label>
      <select class="form-control" name="primaryLanguage" 
                #primaryLanguage
                (blur)="validatePrimaryLanguage(primaryLanguage.value)"
                (change)="validatePrimaryLanguage(primaryLanguage.value)"
                [(ngModel)]="model.primaryLanguage">
          <option value="default">Select a Language...</option>
          <option *ngFor="let lang of languages">
            {{ lang }}
          </option>
      </select>
    </div>
در اینجا اتصال به class.has-error را همانند مثال‌های قبلی مشاهده می‌کنید. اما اینبار به یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت متصل شده‌است؛ زیرا Angular در مورد این المان خاص، کاری را برای ما انجام نخواهد داد. همچنین پیاده سازی حالت عبور از این کامپوننت با Tab نیز توسط اتصال به رخداد blur صورت گرفته‌است تا هر زمانیکه این فیلد focus را از دست داد، اجرا شود. دسترسی به مقدار جاری انتخابی این select نیز توسط یک template reference variable به نام primaryLanguage# صورت گرفته‌است. به این ترتیب می‌توان به خواص این المان مانند value آن دسترسی پیدا کرد. همچنین چون می‌خواهیم با انتخاب گزینه‌ی دیگری این علامت خطا رفع شود، این متد به رخداد change نیز علاوه بر blur، متصل شده‌است.
export class EmployeeRegisterComponent {
  hasPrimaryLanguageError = false;

  validatePrimaryLanguage(value) {
    if (value === 'default'){
      this.hasPrimaryLanguageError = true;
      }
    else{
      this.hasPrimaryLanguageError = false;
      }
  }
}
کار این متد تغییر مقدار hasPrimaryLanguageError است تا کلاس has-error در فرم نمایش داده شود. در اینجا اگر مقدار primaryLanguage انتخابی همان گزینه‌ی default ابتدایی باشد، خاصیت hasPrimaryLanguageError به true تنظیم می‌شود:



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

تا اینجا بررسی‌هایی را که انجام دادیم، در سطح فیلدها بودند. اکنون اگر کاربر به طور کامل تمام این فیلدها را تغییر ندهد و بر روی دکمه‌ی ارسال کلیک کند چطور؟
<form #form="ngForm" novalidate>
زمانیکه فرم جاری را ایجاد کردیم، یک template reference variable را به نام form# نیز به آن نسبت دادیم و چون به ngForm متصل شده‌است، می‌توان به خواص این شیء نیز دسترسی یافت. برای نمونه پس از دکمه‌ی ثبت، عبارت ذیل را تعریف کنید:
<h3> form.valid: {{ form.valid }}</h3>
همانطور که ملاحظه می‌کنید یکی از خواص شیء ngForm که توسط متغیر form قابل دسترسی شده‌است، خاصیت valid می‌باشد که حاصل اعتبارسنجی تمام عناصر داخل فرم است. برای مثال می‌توان از این خاصیت جهت فعال یا غیرفعال کردن دکمه‌ی ثبت اطلاعات نیز استفاده کرد:
<button class="btn btn-primary" type="submit"
            [disabled]="form.invalid">Ok</button>
در اینجا مقدار form.invalid به خاصیت disabled المان متصل شده‌است و اگر اعتبارسنجی فرم با شکست مواجه شود، این دکمه در حالت غیرفعال ظاهر می‌شود.


در قسمت بعد نحوه‌ی ارسال این فرم را به سرور، بررسی می‌کنیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
امکان انجام محاسبات سمت کلاینت در EF Core
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی.  در حالت اول با <IEnumerable<T‌ها کار می‌کنیم که تمام عملیات در حافظه انجام می‌شود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبان‌های متفاوتی مانند SQL ترجمه می‌شوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع داده‌ی آن‌ها دقت کنیم، می‌توانیم نوع آن‌ها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که می‌تواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
var blogs = from blog in Blogs
   where blog.Name.Contains("Development")
   select blog;
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه می‌تواند بر اساس بانک‌های اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام می‌شود.
اما اکنون چطور؟
var blogs = from blog in Blogs
   where blog.Name.ComputeHash() == 0
   select blog;
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کرده‌ایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمه‌ی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئری‌هایی بهبود یافته‌است که به آن، محاسبات سمت کلاینت گفته می‌شود.


یک مثال: بررسی تاثیر ارزیابی‌های سمت کلاینت در EF Core

فرض کنید ساختار جدول بلاگ‌ها به صورت زیر است:
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }  
}
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کرده‌ایم:
    public static class StringExtensions
    {
        public static int ComputeHash(this string str)
        {
            var hash = 0;
            foreach (var ch in str)
            {
                hash += (int)ch;
            }
            return hash;
        }
    }
اکنون می‌خواهیم بلاگ‌هایی را پیدا کنیم که Hash مربوط به Url آن‌ها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
using (var context = new BloggingContext())
{
   var blogs = context.Blogs
     .Where(blog => blog.Url.ComputeHash() >= 10)
     .ToList();
   Console.WriteLine(blogs.First().Url);
}
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمی‌کند:
SELECT [blog].[BlogId], [blog].[Url]
   FROM [Blogs] AS [blog]
به این معنا که در ارزیابی‌های سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده می‌کند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آن‌را از کوئری حذف می‌کند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده می‌شود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کرده‌است، متد ComputeHash را در سمت کلاینت بر روی این نتیجه‌ی دریافتی اعمال می‌کند. یعنی مرحله‌ی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.


چگونه متوجه شویم که ارزیابی سمت کلاینت رخ داده‌است؟

EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابی‌های سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs
   .Select(b => new
   {
      IdUrlString = string.Join(", ", b.BlogId, b.Url),
   }).ToList();
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا می‌شود، اما بهتر است از وقوع یک چنین رخ‌دادهایی مطلع شویم:
    public class BloggingContext : DbContext
    {
        public BloggingContext()
        { }

        public BloggingContext(DbContextOptions options)
            : base(options)
        { }

        public DbSet<Blog> Blogs { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured)
            {
                optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;");
                optionsBuilder.ConfigureWarnings(warnings =>
                {
                    warnings.Log(CoreEventId.IncludeIgnoredWarning);
                    warnings.Log(RelationalEventId.QueryClientEvaluationWarning);
                });
            }
        }
    }
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ می‌شود:
 warn: Microsoft.EntityFrameworkCore.Query[200500]
The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
عنوان کرده‌است که قابلیت ترجمه‌ی ComputeHash را به SQL نداشته و آن‌را در نهایت به صورت محلی و در سمت کلاینت محاسبه می‌کند.

اگر می‌خواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابی‌ها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).


تاثیر ارزیابی‌های سمت کلاینت بر روی کارآیی برنامه

هرچند قابلیت ارزیابی‌های سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبه‌ی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال می‌شود.
ب) این قابلیت ممکن است برنامه نویس‌ها را از تفکر در مورد یافتن روش‌های محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا می‌شود، اما اگر آن‌را به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs
   .Select(b => new
   {
     IdUrlString = b.BlogId + "," + b.Url
   }).ToList();
اینبار به یک خروجی SQL قابل محاسبه‌ی در سمت سرور، خواهیم رسید:
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString]
FROM [Blogs] AS [b]
به همین جهت حداقل لاگ کردن ارزیابی‌های سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: ClientSideEvaluation.zip