نظرات مطالب
کار با Areas در ASP.NET Core
چند نکته در مورد ViewComponentهای قرار گرفته‌ی در Areaها

الف) نیازی به ویژگی Area ندارند و در تمام قسمت‌های برنامه قابل دسترسی هستند.
ب) اگر یک ViewComponent را در یک Area تعریف کردید، نحوه‌ی مسیردهی View آن باید مطلق باشد:
 return View(viewName: "~/Areas/AreaName/Views/Shared/Components/OnlineUsers/Default.cshtml",model: model);
در غیر اینصورت زمانیکه این ViewComponent قرار است در قسمتی از صفحه که متعلق به این Area نیست رندر شود، پیام یافت نشدن View را مشاهده خواهید کرد.
ج) در لینک‌های ViewComponentهای یک Area، ذکر asp-area ضروری است. چون این ViewComponent ممکن است خارج از این Area نیز نمایش داده شود. ذکر asp-area برای لینک دادن به داخل همان Area غیرضروری است؛ اما در اینجا محل یک ViewComponent می‌تواند در کنار صفحه و در کنار تمام صفحات سایت باشد.
مطالب
غیرفعال کردن کش مرورگر هنگام استفاده از jQuery Ajax

برای استفاده از قابلیت‌های Ajax کتابخانه jQuery ، شش متد زیر در اختیار برنامه نویس‌ها است:

$.ajax(), load(), $.get(), $.getJSON(), $.getScript(), and $.post()
که در حقیقت 5 مورد آخر ذکر شده صرفا بیان اولین متد ajax فوق به نحوی دیگر می‌باشند و محصور کننده‌ توانایی‌های آن هستند.

برای مثال کد زیر زمان جاری را از سرور دریافت کرده و نتیجه را در سه تکست باکس قرار داده شده در صفحه نمایش می‌دهد.
ابتدا وب سرویس ساده زیر را در نظر بگیرید که زمان شمسی جاری را بر می‌گرداند:

using System;
using System.Globalization;
using System.Web.Script.Services;
using System.Web.Services;

namespace TestJQueryAjax
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[ScriptService]
public class AjaxSrv : WebService
{
public class TimeInfo
{
public string Date { set; get; }
public string Hr { set; get; }
public string Min { set; get; }
}

[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public TimeInfo GetTime()
{
DateTime now = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day);
PersianCalendar pc = new PersianCalendar();
string today = string.Format("{0}/{1}/{2}",
pc.GetYear(now),
pc.GetMonth(now).ToString("00"),
pc.GetDayOfMonth(now).ToString("00"));
TimeInfo ti = new TimeInfo
{
Date = today,
Hr = DateTime.Now.Hour.ToString("00"),
Min = DateTime.Now.Minute.ToString("00")
};
return ti;
}
}
}
سپس اگر از تابع Ajax کتابخانه jQuery جهت دریافت زمان جاری از وب سرویس استفاده نمائیم، کد صفحه ما به صورت زیر خواهد بود:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestFillCtrls.aspx.cs"
Inherits="TestJQueryAjax.TestFillCtrls" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="js/jquery.js" type="text/javascript"></script>

<script type="text/javascript">
function validate() {
$.ajax({
type: "POST",
url: 'AjaxSrv.asmx/GetTime',
data: '{}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
$("#<%=txtDate.ClientID %>").val(msg.d.Date);
$("#<%=txtHr.ClientID %>").val(msg.d.Hr);
$("#<%=txtMin.ClientID %>").val(msg.d.Min);
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
alert("خطایی رخ داده است");
}
});
//debugger;
}
</script>

</head>
<body>
<form id="form1" runat="server">
<div>
<asp:TextBox ID="txtDate" runat="server" />
<br />
<asp:TextBox ID="txtHr" runat="server" />
<br />
<asp:TextBox ID="txtMin" runat="server" />
<br />
<asp:Button ID="btnGetTime" runat="server" Text="Click here!" UseSubmitBehavior="false"
OnClientClick="validate();return false;" />
</div>
</form>
</body>
</html>
تنها نکته‌ی جدید این اسکریپت، نحوه‌ی استفاده از خروجی JSON وب متد ما است که از نوع TimeInfo تعریف شده است. خروجی نمونه این وب متد به صورت زیر می‌تواند باشد:

{"d":{"__type":"TestJQueryAjax.AjaxSrv+TimeInfo","Date":"1388/07/14","Hr":"12","Min":"59"}}
که نحوه‌ی دسترسی به اجزای آن‌را در متد validate‌ ملاحظه می‌نمائید.

باید به خاطر داشت که برای هر 6 متد Ajax ایی jQuery ، عملیات کش شدن اطلاعات در مرورگر کاربر به صورت پیش فرض فعال است. اما این نکته تنها زمانیکه dataType مورد استفاده از نوع script یا jsonp باشد، صادق نبوده و کش شدن به صورت خودکار غیرفعال می‌گردد.
روش سنتی غیرفعال کردن کش در حین عملیات اجکسی، استفاده از یک کوئری استرینگ متغیر در پایان url درخواستی است. به این صورت مرورگر درخواست صادره را جدید فرض کرده و از کش خود استفاده نمی‌نماید (همین مورد در حالت کش شدن تصاویر هم صادق است).
jQuery نیز همین عملیات را در پشت صحنه انجام داده اما تنظیم آن‌را به نحوی مطلوب‌تری ارائه می‌دهد. یا پارامتر cache را در تعریف متد ajax خود اضافه نموده و مقدار آن را مساوی false قرار دهید و یا جهت تاثیر گذاری بر روی کلیه متدهای مورد استفاده، پیش از استفاده از آن‌ها این تنظیم را مشخص سازید:

$.ajaxSetup({cache: false});

مطالب
استفاده از ماژول Remote
همانطور که در مقاله «آغاز کار با الکترون» گفتیم، فرآیند اصلی، تنها فرآیندی است که توانایی استفاده از گرافیک بومی سیستم عامل را دارد. ولی بسیاری از اوقات نیاز است در سمت renderProcess توانایی انجام این کار‌ها را داشته باشیم. در این مقاله قصد داریم که همان دیالوگ‌های open و save را از طریق Render Process اجرا نماییم.
الکترون برای اینکار از یک ماژول به نام remote استفاده می‌کند که وظیفه آن برقراری ارتباط IPC از Render Process به Main Process است و مواردی را که لازم است، در اختیار شما قرار می‌دهد. در این شیوه لازم نیست شما مرتبا به ارسال پیام بپردازید، بلکه این ارتباطات را ماژول remote فراهم می‌کند. این مورد شبیه به سیستم RMI در جاواست.

برای استفاده از remote در فایل html، کدهای زیر را در تگ اسکریپت اضافه می‌کنیم:
  const remote=require("electron").remote;
    const dialog=remote.dialog;
اینبار هم مانند قسمت قبلی، کدها را به شیوه دیگری انتساب دادیم. قصد ما از تغییر این رویه این است که با انواع حالت‌های انتساب اشیاء، آشنا شویم. بعد از آن توابع زیر را اضافه می‌کنیم:
  function OpenDialog()
    {
      dialog.showOpenDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']},
        {name:'جهت تست' , extensions:['doc','docx']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=  fs.readFileSync(String(filename),'utf8');
             document.getElementById("TextFile").value=content;
    });
    }

    function SaveDialog()
    {
      dialog.showSaveDialog({
        title:'باز کردن فایل متنی',
         properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ]
        ,filters:[
        {name:'فایل‌های نوشتاری' , extensions:['txt','text']}
         ]
      },
        (filename)=>{
          if(filename===undefined)
             return;
             var content=document.getElementById("TextFile").value;
             fs.writeFileSync(String(filename),content,'utf8');
       });
    }
برای استفاده از این توابع، کدهای زیر را نیز به فایل اضافه می‌کنیم تا دکمه‌های open و save به صفحه اضافه شوند:
<button onclick="OpenDialog();" > Open File</button>
<button onclick="SaveDialog();" > Save File</button>
حالا برنامه را اجرا و تست کنید.

عبارت remote شامل متدهای فراوانی است که تعدادی از آن‌ها را بر می‌شماریم:
remote.getCurrentWindow()
شیء BrowserWindow صفحه جاری را باز می‌گرداند.

remote.getCurrentWebContents()
شیء webContents صفحه جاری را باز می‌گرداند.

remote.getGlobal(name)
این متد، دسترسی به شیء global را داراست و یکی از اشیاء ارتباطی بین Main Process و RenderProcess است که می‌تواند هر نوع داده‌ای را جابجا نماید. برای مشاهده بهتر از نحوه کارکرد این متد کد زیر را مشاهده نمایید:
Main Process
global.testData={year:1395};

Render Process
alert(remote.getGlobal("testData").year);
از این پس هر موقع renderProcess به این کد برسد، پیام 1395 را روی صفحه نمایش خواهد داد.

remote.process
شیء، process را از main process دریافت می‌کند و با کد زیر برابر است. ولی مزیت این متد این است که از کش نیز استفاده می‌نماید.
remote.getGlobal('process')

در مورد شیء process باید گفت که شامل خصوصیات و متدهایی در مورد پروسه اصلی اپلیکیشن می‌باشد. این اطلاعات مثل دریافت شماره نسخه الکترون، شماره نسخه کرومیوم، دریافت اطلاعات حافظه در مورد پروسه اپلیکیشن و حتی دریافت اطلاعات حافظه در مورد کل سیستم و ... می‌شود.
نظرات مطالب
Angular CLI - قسمت اول - نصب و راه اندازی
یک نکته‌ی تکمیلی: پشتیبانی توکار از به روز رسانی بسته‌های Angular
به همراه Angular CLI نگارش 1.7، دستور جدیدی به نام ng update اضافه شده‌است که تمام وابستگی‌های مرتبط به Angular را به صورت خودکار به روز رسانی می‌کند.
مطالب
نحوه صحیح تولید Url در ASP.NET MVC
کار متد کمکی Url.Action ایجاد یک Url بر اساس تعاریف مسیریابی برنامه است. البته متد کمکی مشابهی نیز مانند Html.ActionLink در ASP.NET MVC وجود دارد که کار آن تولید یک لینک قابل کلیک است؛ اما ممکن است در حالتی خاص تنها نیاز به خود Url داشته باشیم و نه لینک قابل کلیک آن.

الف) اگر از jQuery Ajax استفاده می‌کنید، حتما باید استفاده از Url.Action را لحاظ کنید

برای نمونه اگر قسمتی از عملیات Ajaxایی برنامه شما به نحو زیر تعریف شده است :
$.ajax({  
           type: "POST",  
            url: "/Home/EmployeeInfo",  
...
... غلط است!
در این حالت برنامه شما تنها در زمانیکه در ریشه یک دومین قرار گرفته باشد کار خواهد کرد. اگر برنامه شما در مسیری مانند http://www.site.com/myNewApp نصب شود، کلیه فراخوانی‌های Ajax ایی آن دیگر کار نخواهند کرد چون مسیر url فوق به ریشه سایت اشاره می‌کند و نه مسیر جاری برنامه شما (در یک sub domain جدید).
به همین جهت در یک چنین حالتی حتما باید به کمک Url.Action مسیر یک اکشن متد را معرفی کرد تا به صورت خودکار بر اساس محل قرارگیری برنامه و تعاریف مسیریابی آن، Url صحیحی تولید شود.

@Url.Action("EmployeeInfo", "Home")

ب) دریافت Url مطلق از یک Url.Action

Urlهای تولید شده توسط Url.Action به صورت پیش فرض نسبی هستند (نسبت به محل نصب و قرارگیری برنامه تعریف می‌شوند). اگر نیاز به دریافت یک مسیر مطلق که با http برای مثال شروع می‌شود دارید، باید به نحو زیر عمل کرد:
@Url.Action("About", "Home", null, "http")
پارامتر چهارم تعریف شده، سبب خواهد شد لینک تولیدی از حالت نسبی خارج شود.


ج) استفاده از Url.Action در یک کنترلر

فرض کنید قصد تولید یک فید RSS را در کنترلری دارید. یکی از آیتم‌هایی که باید ارائه دهید، لینک به مطلب مورد نظر است. این لینک باید مطلق باشد همچنین در یک View هم قرار نداریم که به کمک @ بلافاصله به متد کمکی Url.Action دسترسی پیدا کنیم.
در کنترلرها، وهله جاری کلاس به شیء Url و متد Action آن به نحو زیر دسترسی دارد و خروجی نهایی آن یک رشته است:

var url = this.Url.Action(actionName: "Index",
                                  controllerName: "Post",
                                  protocol: "http",
                                  routeValues: new { id = item.Id });
بنابراین در کنترلرها نیز Urlها را دستی تولید نکنید و اجازه دهید بر اساس پارامترهای زیادی که در پشت صحنه Url.Action لحاظ می‌شود، Url صحیحی برای شما تولید گردد.


د) استفاده از Url.Action در کلاس‌های کمکی برنامه خارج از یک کنترلر

فرض کنید قصد تهیه یک Html Helper سفارشی را به کمک کدنویسی در یک کلاس مجزا دارید. در اینجا نیز نباید Urlها را دستی تولید کرد. در Html Helperهای سفارشی نیز می‌توان به کمک متد  UrlHelper.GenerateUrl، به همان امکانات Url.Action دسترسی یافت:

public static class Extensions  
    {  
        public static string MyLink(this HtmlHelper html, ...)  
        {  
            string url = UrlHelper.GenerateUrl(null, "actionName", "controllerName",
                                                null,
                                                html.RouteCollection,
                                                html.ViewContext.RequestContext,
                                                includeImplicitMvcValues: true);
//...
 
مطالب
Blazor 5x - قسمت 20 - کار با فرم‌ها - بخش 8 - استفاده از یک کامپوننت ثالث HTML Editor
در این قسمت می‌خواهیم بجای دریافت اطلاعات توضیحات یک اتاق، توسط یک text area متداول، برای مثال از Quill rich text editor استفاده کنیم. برای این منظور می‌توان از کامپوننت Blazor محصور کننده‌ی آن به نام Blazored TextEditor کمک گرفت.


نصب کامپوننت Blazored TextEditor

ابتدا نیاز است بسته‌ی نیوگت آن‌را با اجرای دستور زیر، به پروژه‌ی Blazor خود اضافه کرد:
dotnet add package Blazored.TextEditor
و همچنین کتابخانه‌ی اصلی quill را نیز در مسیر wwwroot/lib/quill نصب می‌کنیم:
libman install quill --provider unpkg --destination wwwroot/lib/quill
سپس به فایل Pages\_Host.cshtml مراجعه کرده و ابتدا مداخل تعریف فایل‌های CSS آن‌را اضافه می‌کنیم:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/quill/dist/quill.snow.css" rel="stylesheet" />
    <link href="lib/quill/dist/quill.bubble.css" rel="stylesheet" />
و در ادامه سه مدخل اسکریپتی زیر را نیز به قسمت پیش از بسته شدن تگ body، اضافه می‌کنیم:
 <script src="lib/quill/dist/quill.min.js"></script>
<script src="_content/Blazored.TextEditor/quill-blot-formatter.min.js"></script>
<script src="_content/Blazored.TextEditor/Blazored-BlazorQuill.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
اگر برنامه‌ی مورد نظر از نوع Blazor WASM است، این تنظیمات به فایل wwwroot\index.html منتقل می‌شوند.

و در آخر جهت سهولت کار با این کامپوننت می‌توان فضای نام آن‌را به فایل BlazorServer.App\_Imports.razor به صورت زیر اضافه کرد:
@using Blazored.TextEditor


استفاده از کامپوننت Blazored.TextEditor در کامپوننت HotelRoomUpsert.razor

می‌خواهیم در کامپوننت HotelRoomUpsert.razor مثال این سری، بجای کامپوننت InputTextArea مورد استفاده، از یک HTML Editor استفاده کنیم:
<div class="form-group">
    <label>Details</label>
    @*<InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>*@
    <BlazoredTextEditor @ref="@QuillHtml">
        <ToolbarContent>
            <select class="ql-header">
                <option selected=""></option>
                <option value="1"></option>
                <option value="2"></option>
                <option value="3"></option>
                <option value="4"></option>
                <option value="5"></option>
            </select>
            <span class="ql-formats">
                <button class="ql-bold"></button>
                <button class="ql-italic"></button>
                <button class="ql-underline"></button>
                <button class="ql-strike"></button>
            </span>
            <span class="ql-formats">
                <select class="ql-color"></select>
                <select class="ql-background"></select>
            </span>
            <span class="ql-formats">
                <button class="ql-list" value="ordered"></button>
                <button class="ql-list" value="bullet"></button>
            </span>
            <span class="ql-formats">
                <button class="ql-link"></button>
            </span>
        </ToolbarContent>
        <EditorContent>
        </EditorContent>
    </BlazoredTextEditor>
</div>
- در اینجا قسمت محتوای EditorContent مثال آن‌را خالی کرده‌ایم.
- همانطور که ملاحظه می‌کنید، این تعریف به همراه یک ارجاع به وهله‌ای از آن نیز هست:
<BlazoredTextEditor @ref="@QuillHtml">
به همین جهت نیاز است فیلد متناظر با آن‌را در قسمت کدهای کامپوننت، به صورت زیر تعریف کرد:
@code
{
   private BlazoredTextEditor QuillHtml;
تا اینجا اگر برنامه را اجرا کنیم، به خروجی زیر می‌رسیم:


برای تغییر اندازه و مقدار placeholder پیش‌فرض آن، می‌توان به صورت زیر عمل کرد:
<div class="form-group pb-4" style="height:250px;">
    <label>Details</label>
    <BlazoredTextEditor @ref="@QuillHtml" Placeholder="Please enter the room's detail">


تنظیم و دریافت متن نمایشی HTML Editor

مطابق مستندات این کامپوننت، روش تنظیم متن نمایشی آن، به کمک متد LoadHTMLContent است. به همین جهت متد زیر را به کدهای کامپوننت جاری اضافه می‌کنیم:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
        }
    }
بنابراین روش متداول two-way binding در اینجا کار نمی‌کند و باید متن این ادیتور را به نحو فوق تنظیم کرد و برای مثال در زمان بارگذاری اولیه‌ی این کامپوننت و در حالت ویرایش، متن دریافتی از بانک اطلاعاتی را به ادیتور فوق ارسال نمود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
            await SetHTMLAsync();
        }

        // ... 
    }
و یا در زمان ثبت اولیه و یا حتی در حالت ویرایش اطلاعات در متد HandleHotelRoomUpsert، با استفاده از متد GetHTML آن، خاصیت HotelRoomModel.Details را مقدار دهی اولیه کرد:
    private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       HotelRoomModel.Details = await QuillHtml.GetHTML();

       // ...
    }

مشکل! ادیتور در زمان ویرایش یک رکورد، اطلاعات پیشین را نمایش نمی‌دهد!

پس از اعمال تغییرات فوق، برنامه را اجرا می‌کنیم. سپس یک اتاق جدید را اضافه کرده و در لیست نمایش اتاق‌ها، گزینه‌ی ویرایش آن‌را انتخاب می‌کنیم. در این حالت هرچند کار مقدار دهی HotelRoomModel.Details در زمان ثبت اطلاعات انجام شده، اما ... در زمان ویرایش چیزی نمایش داده نمی‌شود و تغییراتی را که به متد رویدادگردان OnInitializedAsync اضافه کرده‌ایم، عمل نمی‌کنند.
در این مورد در قسمت بررسی چرخه‌ی حیات کامپوننت‌ها توضیحاتی ابتدایی ارائه شد:
«رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.»

بنابراین در این حالت خاص که ادیتور جاوا اسکریپتی مورد استفاده، پس از رندر کامل UI نمایش داده می‌شود، قرار دادن متد SetHTML در روال رویدادگردان OnInitializedAsync کار نخواهد کرد و باید آن‌را به روال رویدادگردان OnAfterRender انتقال دهیم:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
   await SetHTMLAsync();
}
پس از این تغییرات هم باز متن وارد شده‌ی در قسمت توضیحات، در حالت ویرایش نمایش داده نمی‌شود! علت آن‌را نیز در مطلب بررسی چرخه‌ی حیات کامپوننت‌ها بررسی کردیم: «یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.» به همین جهت نیاز به فراخوانی دستی StateHasChanged وجود دارد:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
            StateHasChanged();
        }
    }


مشکل! اگر در این حالت سعی کنیم متنی را در ادیتور وارد کنیم، میسر نیست و همچنین CPU Usage سیستم به 100 درصد رسیده‌است!

علت اینجا است که فراخوانی StateHasChanged، هر چند سبب رندر مجدد UI می‌شود، اما چون در پایان کار رندر قرار داریم، یک حلقه‌ی بی‌نهایت را سبب خواهد شد. به همین جهت باید در متد OnAfterRenderAsync، بر اساس پارامتر firstRender، از رندرهای بعدی جلوگیری کرد:
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        while (true)
        {
            try
            {
                await SetHTMLAsync();
                break;
            }
            catch
            {
                await Task.Delay(100); // Quill needs some time to load
            }
        }
    }
در اینجا هم مدیریت firstRender را مشاهده می‌کنید، تا دیگر یک حلقه‌ی بی‌نهایت رخ ندهد و هم حلقه‌ای را جهت منتظر ماندن تا بارگذاری کامل Quill در این مثال. این افزونه‌ی جاوا اسکریپتی، حتی پس از پایان رندر کامپوننت هم نیاز به مدت زمانی دارد تا بتواند کامل بارگذاری شده و قابل استفاده شود.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-20.zip
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت چهاردهم- آماده شدن برای انتشار برنامه
در «قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی»، اطلاعات TestUser تنظیم شده‌ی در کلاس Config برنامه‌ی IDP را به بانک اطلاعاتی منتقل کردیم که در نتیجه‌ی آن سه جدول Users، UserClaims و UserLogins، تشکیل شدند. در اینجا می‌خواهیم سایر قسمت‌های کلاس Config را نیز به بانک اطلاعاتی منتقل کنیم.


تنظیم مجوز امضای توکن‌های IDP

namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
تا اینجا تنظیمات کلاس آغازین برنامه چنین شکلی را دارد که AddCustomUserStore آن‌را در قسمت دهم به آن افزودیم.
مرحله‌ی بعد، تغییر AddDeveloperSigningCredential به یک نمونه‌ی واقعی است. استفاده‌ی از روش فعلی آن چنین مشکلاتی را ایجاد می‌کند:
- اگر برنامه‌ی IDP را در سرورهای مختلفی توزیع کنیم و این سرورها توسط یک Load balancer مدیریت شوند، هر درخواست رسیده، به سروری متفاوت هدایت خواهد شد. در این حالت هر برنامه نیز مجوز امضای توکن متفاوتی را پیدا می‌کند. برای مثال اگر یک توکن دسترسی توسط سرور A امضاء شود، اما در درخواست بعدی رسیده، توسط مجوز سرور B تعیین اعتبار شود، این اعتبارسنجی با شکست مواجه خواهد شد.
- حتی اگر از یک Load balancer استفاده نکنیم، به طور قطع Application pool برنامه در سرور، در زمانی خاص Recycle خواهد شد. این مورد DeveloperSigningCredential تنظیم شده را نیز ریست می‌کند. یعنی با ری‌استارت شدن Application pool، کلیدهای مجوز امضای توکن‌ها تغییر می‌کنند که در نهایت سبب شکست اعتبارسنجی توکن‌های صادر شده‌ی توسط IDP می‌شوند.

بنابراین برای انتشار نهایی برنامه نمی‌توان از DeveloperSigningCredential فعلی استفاده کرد و نیاز است یک signing certificate را تولید و تنظیم کنیم. برای این منظور از برنامه‌ی makecert.exe مایکروسافت که جزئی از SDK ویندوز است، استفاده می‌کنیم. این فایل را از پوشه‌ی src\IDP\DNT.IDP\MakeCert نیز می‌توانید دریافت کنید.
سپس دستور زیر را با دسترسی admin اجرا کنید:
 makecert.exe -r -pe -n "CN=DntIdpSigningCert" -b 01/01/2018 -e 01/01/2025 -eku 1.3.6.1.5.5.7.3.3 -sky signature -a sha256 -len 2048 -ss my -sr LocalMachine
در اینجا تاریخ شروع و پایان اعتبار مجوز ذکر شده‌اند. همچنین نتیجه‌ی آن به صورت خودکار در LocalMachine certificate store ذخیره می‌شود. به همین جهت اجرای آن نیاز به دسترسی admin را دارد.
پس از آن در قسمت run ویندوز، دستور mmc را وارد کرده و enter کنید. سپس از منوی File گزینه‌ی Add remove span-in را انتخاب کنید. در اینجا certificate را add کنید. در صفحه‌ی باز شده Computer Account و سپس Local Computer را انتخاب کنید و در نهایت OK. اکنون می‌توانید این مجوز جدید را در قسمت «Personal/Certificates»، مشاهده کنید:


در اینجا Thumbprint این مجوز را در حافظه کپی کنید؛ از این جهت که در ادامه از آن استفاده خواهیم کرد.

چون این مجوز از نوع self signed است، در قسمت Trusted Root Certification Authorities قرار نگرفته‌است که باید این انتقال را انجام داد. در غیراینصورت می‌توان توسط آن توکن‌های صادر شده را امضاء کرد اما به عنوان یک توکن معتبر به نظر نخواهند رسید.
در ادامه این مجوز جدید را انتخاب کرده و بر روی آن کلیک راست کنید. سپس گزینه‌ی All tasks -> export را انتخاب کنید. نکته‌ی مهمی را که در اینجا باید رعایت کنید، انتخاب گزینه‌ی «yes, export the private key» است. کپی و paste این مجوز از اینجا به جایی دیگر، این private key را export نمی‌کند. در پایان این عملیات، یک فایل pfx را خواهید داشت.
- در آخر نیاز است این فایل pfx را در مسیر «Trusted Root Certification Authorities/Certificates» قرار دهید. برای اینکار بر روی نود certificate آن کلیک راست کرده و گزینه‌ی All tasks -> import را انتخاب کنید. سپس مسیر فایل pfx خود را داده و این مجوز را import نمائید.

پس از ایجاد مجوز امضای توکن‌ها و انتقال آن به Trusted Root Certification Authorities، نحوه‌ی معرفی آن به IDP به صورت زیر است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients());
        }

        private X509Certificate2 loadCertificateFromStore()
        {
            var thumbPrint = Configuration["CertificateThumbPrint"];
            using (var store = new X509Store(StoreName.My, StoreLocation.LocalMachine))
            {
                store.Open(OpenFlags.ReadOnly);
                var certCollection = store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, true);
                if (certCollection.Count == 0)
                {
                    throw new Exception("The specified certificate wasn't found.");
                }
                return certCollection[0];
            }
        }

        private X509Certificate2 loadCertificateFromFile()
        {
            // NOTE:
            // You should check out the identity of your application pool and make sure
            // that the `Load user profile` option is turned on, otherwise the crypto susbsystem won't work.
            var certificate = new X509Certificate2(
                fileName: Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data", 
                    Configuration["X509Certificate:FileName"]),
                password: Configuration["X509Certificate:Password"],
                keyStorageFlags: X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet |
                                 X509KeyStorageFlags.Exportable);
            return certificate;
        }
    }
}
متد کمکی loadCertificateFromStore، بر اساس thumbPrint مجوز تولید شده، آن‌را بارگذاری می‌کند. سپس این مجوز، توسط متد AddSigningCredential به IdentityServer معرفی خواهد شد و یا اگر فایل pfx ای را دارید، می‌توانید از متد loadCertificateFromFile استفاده کنید. این متد برای اینکه در IIS به درستی کار کند، نیاز است در خواص Application pool سایت IDP، گزینه‌ی Load user profile را انتخاب کرده باشید (مهم!).

پس از این تغییرات، برنامه را اجرا کنید. سپس مسیر discovery document را طی کرده و آدرس jwks_uri آن‌را در مرورگر باز کنید. در اینجا خاصیت kid نمایش داده شده با thumbPrint مجوز یکی است.
https://localhost:6001/.well-known/openid-configuration
https://localhost:6001/.well-known/openid-configuration/jwks


انتقال سایر قسمت‌های فایل Config برنامه‌ی IDP به بانک اطلاعاتی

قسمت آخر آماده سازی برنامه برای انتشار آن، انتقال سایر داده‌های فایل Config، مانند Resources و Clients برنامه‌ی IDP، به بانک اطلاعاتی است. البته هیچ الزامی هم به انجام اینکار نیست. چون اگر تعداد برنامه‌های متفاوتی که در سازمان قرار است از IDP استفاده کنند، کم است، تعریف مستقیم آن‌ها داخل فایل Config برنامه‌ی IDP، مشکلی را ایجاد نمی‌کند و این تعداد رکورد الزاما نیازی به بانک اطلاعاتی ندارند. اما اگر بخواهیم امکان به روز رسانی این اطلاعات را بدون نیاز به کامپایل مجدد برنامه‌ی IDP توسط یک صفحه‌ی مدیریتی داشته باشیم، نیاز است آن‌ها را به بانک اطلاعاتی منتقل کنیم. این مورد مزیت به اشتراک گذاری یک چنین اطلاعاتی را توسط Load balancers نیز میسر می‌کند.
البته باید درنظر داشت قسمت دیگر اطلاعات IdentityServer شامل refresh tokens و reference tokens هستند. تمام این‌ها اکنون در حافظه ذخیره می‌شوند که با ری‌استارت شدن Application pool برنامه از بین خواهند رفت. بنابراین حداقل در این مورد استفاده‌ی از بانک اطلاعاتی اجباری است.
خوشبختانه قسمت عمده‌ی این کار توسط خود تیم IdentityServer توسط بسته‌ی IdentityServer4.EntityFramework انجام شده‌است که در اینجا از آن استفاده خواهیم کرد. البته در اینجا این بسته‌ی نیوگت را مستقیما مورد استفاده قرار نمی‌دهیم. از این جهت که نیاز به 2 رشته‌ی اتصالی جداگانه و دو Context جداگانه را دارد که داخل خود این بسته تعریف شده‌است و ترجیح می‌دهیم که اطلاعات آن‌را با ApplicationContext خود یکی کنیم.
برای این منظور آخرین سورس کد پایدار آن‌را از این آدرس دریافت کنید:
https://github.com/IdentityServer/IdentityServer4.EntityFramework/releases

انتقال موجودیت‌ها به پروژه‌ی DNT.IDP.DomainClasses

در این بسته‌ی دریافتی، در پوشه‌ی src\IdentityServer4.EntityFramework\Entities آن، کلاس‌های تعاریف موجودیت‌های متناظر با منابع IdentityServer قرار دارند. بنابراین همین فایل‌ها را از این پروژه استخراج کرده و به پروژه‌ی DNT.IDP.DomainClasses در پوشه‌ی جدید IdentityServer4Entities اضافه می‌کنیم.
البته در این حالت پروژه‌ی DNT.IDP.DomainClasses نیاز به این وابستگی‌ها را خواهد داشت:
<Project Sdk="Microsoft.NET.Sdk">  
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.ComponentModel.Annotations" Version="4.3.0" />
    <PackageReference Include="IdentityServer4" Version="2.2.0" />
  </ItemGroup>
</Project>
پس از این انتقال، فضاهای نام این کلاس‌ها را نیز اصلاح می‌کنیم؛ تا با پروژه‌ی جاری تطابق پیدا کنند.


انتقال تنظیمات روابط بین موجودیت‌ها، به پروژه‌ی DNT.IDP.DataLayer

در فایل src\IdentityServer4.EntityFramework\Extensions\ModelBuilderExtensions.cs بسته‌ی دریافتی، تعاریف تنظیمات این موجودیت‌ها به همراه نحوه‌ی برقراری ارتباطات بین آن‌ها قرار دارد. بنابراین این اطلاعات را نیز از این فایل استخراج و به پروژه‌ی DNT.IDP.DataLayer اضافه می‌کنیم. البته در اینجا از روش IEntityTypeConfiguration برای قرار هر کدام از تعاریف یک در کلاس مجزا استفاده کرده‌ایم.
پس از این انتقال، به کلاس Context برنامه مراجعه کرده و توسط متد builder.ApplyConfiguration، این فایل‌های IEntityTypeConfiguration را معرفی می‌کنیم.


تعاریف DbSetهای متناظر با موجودیت‌های منتقل و تنظیم شده در پروژه‌ی DNT.IDP.DataLayer

پس از انتقال موجودیت‌ها و روابط بین آن‌ها، دو فایل DbContext را در این بسته‌ی دریافتی خواهید یافت:
الف) فایل src\IdentityServer4.EntityFramework\DbContexts\ConfigurationDbContext.cs
این فایل، موجودیت‌های تنظیمات برنامه مانند Resources و Clients را در معرض دید EF Core قرار می‌دهد.
سپس فایل src\IdentityServer4.EntityFramework\Interfaces\IConfigurationDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.
ب) فایل src\IdentityServer4.EntityFramework\DbContexts\PersistedGrantDbContext.cs
این فایل، موجودیت‌های ذخیره سازی اطلاعات مخصوص IDP را مانند refresh tokens و reference tokens، در معرض دید EF Core قرار می‌دهد.
همچنین فایل src\IdentityServer4.EntityFramework\Interfaces\IPersistedGrantDbContext.cs نیز جهت استفاده‌ی از این DbContext در سرویس‌های این بسته‌ی دریافتی تعریف شده‌است.

ما در اینجا DbSetهای هر دوی این DbContext‌ها را در ApplicationDbContext خود، خلاصه و ادغام می‌کنیم.


انتقال نگاشت‌های AutoMapper بسته‌ی دریافتی به پروژه‌ی جدید DNT.IDP.Mappings

در پوشه‌ی src\IdentityServer4.EntityFramework\Mappers، تعاریف نگاشت‌های AutoMapper، برای تبدیلات بین موجودیت‌های برنامه و IdentityServer4.Models انجام شده‌است. کل محتویات این پوشه را به یک پروژه‌ی Class library جدید به نام DNT.IDP.Mappings منتقل و فضاهای نام آن‌را نیز اصلاح می‌کنیم.


انتقال src\IdentityServer4.EntityFramework\Options به پروژه‌ی DNT.IDP.Models

در پوشه‌ی Options بسته‌ی دریافتی سه فایل موجود هستند:
الف) Options\ConfigurationStoreOptions.cs
این فایل، به همراه تنظیمات نام جداول متناظر با ذخیره سازی اطلاعات کلاینت‌ها است. نیازی به آن نداریم؛ چون زمانیکه موجودیت‌ها و تنظیمات آن‌ها را به صورت مستقیم در اختیار داریم، نیازی به فایل تنظیمات ثالثی برای انجام اینکار نیست.
ب) Options\OperationalStoreOptions.cs
این فایل، تنظیمات نام جداول مرتبط با ذخیره سازی توکن‌ها را به همراه دارد. به این نام جداول نیز نیازی نداریم. اما این فایل به همراه سه تنظیم زیر جهت پاکسازی دوره‌ای توکن‌های قدیمی نیز هست:
namespace IdentityServer4.EntityFramework.Options
{
    public class OperationalStoreOptions
    {
        public bool EnableTokenCleanup { get; set; } = false;
        public int TokenCleanupInterval { get; set; } = 3600;
        public int TokenCleanupBatchSize { get; set; } = 100;
    }
}
از این تنظیمات در سرویس TokenCleanup استفاده می‌شود. به همین جهت همین سه مورد را به پروژه‌ی DNT.IDP.Models منتقل کرده و سپس بجای اینکه این کلاس را مستقیما در سرویس TokenCleanup تزریق کنیم، آن‌را از طریق سیستم Configuration و فایل appsettings.json به این سرویس تزریق می‌کنیم؛ به کمک سرویس توکار IOptions خود ASP.NET Core:
public TokenCleanup(
  IServiceProvider serviceProvider, 
  ILogger<TokenCleanup> logger, 
  IOptions<OperationalStoreOptions> options)
ج) Options\TableConfiguration.cs
کلاسی است به همراه خواص نام اسکیمای جداول که در دو کلاس تنظیمات قبلی بکار رفته‌است. نیازی به آن نداریم.


انتقال سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP.Services

بسته‌ی دریافتی، شامل دو پوشه‌ی src\IdentityServer4.EntityFramework\Services و src\IdentityServer4.EntityFramework\Stores است که سرویس‌های آن‌را تشکیل می‌دهند (جمعا 5 سرویس TokenCleanup، CorsPolicyService، ClientStore، PersistedGrantStore و ResourceStore). بنابراین این سرویس‌ها را نیز مستقیما از این پوشه‌ها به پروژه‌ی DNT.IDP.Services کپی خواهیم کرد.
همانطور که عنوان شد دو فایل Interfaces\IConfigurationDbContext.cs و Interfaces\IPersistedGrantDbContext.cs برای دسترسی به دو DbContext این بسته‌ی دریافتی در سرویس‌های آن، تعریف شده‌اند و چون ما در اینجا صرفا یک ApplicationDbContext را داریم که از طریق IUnitOfWork، در دسترس لایه‌ی سرویس قرار می‌گیرد، ارجاعات به دو اینترفیس یاد شده را با IUnitOfWork تعویض خواهیم کرد تا مجددا قابل استفاده شوند.


انتقال متدهای الحاقی معرفی سرویس‌های IdentityServer4.EntityFramework به پروژه‌ی DNT.IDP

پس از انتقال قسمت‌های مختلف IdentityServer4.EntityFramework به لایه‌های مختلف برنامه‌ی جاری، اکنون نیاز است سرویس‌های آن‌را به برنامه معرفی کرد که در نهایت جایگزین متدهای فعلی درون حافظه‌ای کلاس آغازین برنامه‌ی IDP می‌شوند. خود این بسته در فایل زیر، به همراه متدهایی الحاقی است که این معرفی را انجام می‌دهند:
src\IdentityServer4.EntityFramework\Extensions\IdentityServerEntityFrameworkBuilderExtensions.cs
به همین جهت این فایل را به پروژه‌ی وب DNT.IDP ، منتقل خواهیم کرد؛ همانجایی که در قسمت دهم AddCustomUserStore را تعریف کردیم.
این کلاس پس از انتقال، نیاز به تغییرات ذیل را دارد:
الف) چون یکسری از کلاس‌های تنظیمات را حذف کردیم و نیازی به آن‌ها نداریم، آن‌ها را نیز به طور کامل از این فایل حذف می‌کنیم. تنها تنظیم مورد نیاز آن، OperationalStoreOptions است که اینبار آن‌را از فایل appsettings.json دریافت می‌کنیم. بنابراین ذکر این مورد نیز در اینجا اضافی است.
ب) در آن، دو Context موجود در بسته‌ی اصلی IdentityServer4.EntityFramework مورد استفاده قرار گرفته‌اند. ما در اینجا آن‌ها را نیز با تک Context برنامه‌ی خود تعویض می‌کنیم.
ج) در آن سرویسی به نام TokenCleanupHost تعریف شده‌است. آن‌را نیز به لایه‌ی سرویس‌ها منتقل می‌کنیم. همچنین در امضای سازنده‌ی آن بجای تزریق مستقیم OperationalStoreOptions از <IOptions<OperationalStoreOptions استفاده خواهیم کرد.
نگارش نهایی و تمیز شده‌ی IdentityServerEntityFrameworkBuilderExtensions را در اینجا مشاهده می‌کنید.


افزودن متدهای الحاقی جدید به فایل آغازین برنامه‌ی IDP

پس از انتقال IdentityServerEntityFrameworkBuilderExtensions به پروژه‌ی DNT.IDP، اکنون نوبت به استفاده‌ی از آن است:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                .AddSigningCredential(loadCertificateFromStore())
                .AddCustomUserStore()
                .AddConfigurationStore()
                .AddOperationalStore();
به این ترتیب متدهای الحاقی جدید AddConfigurationStore و AddOperationalStore جهت معرفی محل‌های ذخیره سازی اطلاعات کاربران، منابع و توکن‌های IdentityServer مورد استفاده قرار می‌گیرند.


اجرای Migrations در پروژه‌ی DNT.IDP.DataLayer

پس از پایان این نقل و انتقالات، اکنون نیاز است ساختار بانک اطلاعاتی برنامه را بر اساس موجودیت‌ها و روابط جدید بین آن‌ها، به روز رسانی کنیم. به همین جهت فایل add_migrations.cmd موجود در پوشه‌ی src\IDP\DNT.IDP.DataLayer را اجرا می‌کنیم تا کلاس‌های Migrations متناظر تولید شوند و سپس فایل update_db.cmd را اجرا می‌کنیم تا این تغییرات، به بانک اطلاعاتی برنامه نیز اعمال گردند.


انتقال اطلاعات فایل درون حافظه‌ای Config، به بانک اطلاعاتی برنامه

تا اینجا اگر برنامه را اجرا کنیم، دیگر کار نمی‌کند. چون جداول کلاینت‌ها و منابع آن خالی هستند. به همین جهت نیاز است اطلاعات فایل درون حافظه‌ای Config را به بانک اطلاعاتی منتقل کنیم. برای این منظور سرویس ConfigSeedDataService را به برنامه اضافه کرده‌ایم:
    public interface IConfigSeedDataService
    {
        void EnsureSeedDataForContext(
            IEnumerable<IdentityServer4.Models.Client> clients,
            IEnumerable<IdentityServer4.Models.ApiResource> apiResources,
            IEnumerable<IdentityServer4.Models.IdentityResource> identityResources);
    }
این سرویس به کمک اطلاعات Mappings مخصوص AutoMapper این پروژه، IdentityServer4.Models تعریف شده‌ی در کلاس Config درون حافظه‌ای را به موجودیت‌های اصلی DNT.IDP.DomainClasses.IdentityServer4Entities تبدیل و سپس در بانک اطلاعاتی ذخیره می‌کند.
برای استفاده‌ی از آن، به کلاس آغازین برنامه‌ی IDP مراجعه می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            initializeDb(app);
            seedDb(app);
    // ...
        }
        
        private static void seedDb(IApplicationBuilder app)
        {
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var configSeedDataService = scope.ServiceProvider.GetService<IConfigSeedDataService>();
                configSeedDataService.EnsureSeedDataForContext(
                    Config.GetClients(),
                    Config.GetApiResources(),
                    Config.GetIdentityResources()
                    );
            }
        }
در اینجا توسط متد seedDb، متدهای درون حافظه‌ای کلاس Config به سرویس ConfigSeedDataService ارسال شده، توسط Mappings تعریف شده، به معادل‌های موجودیت‌های برنامه تبدیل و سپس در بانک اطلاعاتی ذخیره می‌شوند.


آزمایش برنامه

پس از اعمال این تغییرات، برنامه‌ها را اجرا کنید. سپس به بانک اطلاعاتی آن مراجعه نمائید. در اینجا لیست جداول جدیدی را که اضافه شده‌اند ملاحظه می‌کنید:


همچنین برای نمونه، در اینجا اطلاعات منابع منتقل شده‌ی به بانک اطلاعاتی، از فایل Config، قابل مشاهده هستند:


و یا قسمت ذخیره سازی خودکار توکن‌های تولیدی توسط آن نیز به درستی کار می‌کند:





کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
نظرات مطالب
inject$ در AngularJs
 به صورت کلی با استفاده از watch$  می‌توان تمامی تغییراتی را که به خواص ViewModel اعمال می‌شوند مشاهده کرد.
تعریف کلی آن به صورت زیر است:
$watch(watchExpression, listener, objectEquality)
»watchExpression : می‌توان نام خاصیت مورد نظر در ViewModel یا یک تابع را که قصد مشاهده تغییرات آن را داریم تعیین کنیم.
»Listener : با تغییر در مقدار watchExpression  اگر  مقدار قبلی این عبارت با مقدار فعلی آن برابر نباشد این تابع فراخوانی می‌شود.
» objectEquality : به صورت پیش فرض Angular مقادیر مورد نظر برای تغییرات را فقط از نظر Reference Equal بودن چک می‌کند. اگر بخواهیم که Angular به صورت عمقی و درختی مقادیر ابجکت‌ها را بررسی کند مقدار این پارامتر باید true شود.
در فریم ورک Angular هر زمان که عمل مقید سازی خواص ViewModel به عناصر DOM انجام می‌گیرد در واقع یک نمونه از watch$ به لیستی به نام watch list $ اضافه می‌شود. دقت کنید که صرفا تعریف در محدوده کنترلر کافی نیست بلکه باید خاصیت مورد نظر حتما مقید شود. برای مثال
app.controller('MainCtrl', function($scope) {
  $scope.foo = "Foo";
  $scope.world = "World";
});
در View نیز
Hello, {{ World }}
در کنترلر بالا دو خاصیت تعریف شده است، در حالی که در View فقط یک خاصیت مقید شده است. درنتیجه فقط یک watch$ به لیست مورد نظر اضافه شده است.
و به عنوان نکته آخر، در Angular نسخه 1.1.4 تابعی به نام watchCollection اضافه شده است که برای ردیابی تغییرات یک مجموعه مورد استفاده قرار می‌گیرد.
یک مثال در این مورد
مطالب
PHP سریعتر از ASP.NET! افسانه یا واقعیت؟

چرا افسانه‌ای که می‌گوید PHP از ASP.NET سریعتر است اینقدر شایع است؟ در این مقاله به بیان حقایقی می‌پردازیم که این افسانه را زیر سوال می‌برد؟

خیلی وقتها در بسیاری از نوشته‌ها و اظهارنظر‌ها می‌بینیم ادعا می‌شود که PHP بسیار سریعتر از ASP.net است و اینکه ASP.net از لحاظ سرعت کند است. آزار دهنده‌ترین بخش این ادعاها، آن است که هر یک از آنها را که نگاه می‌کنی بصورت کاملا غیر واقع بینانه به موضوع نگاه می‌کنند و فقط بدون دلیل این موضوع را ادعا می‌کنند. زیرا به این موضوع بصورتی کاملا متعصبانه و بدور از واقعیتها نگاه می‌شود. به همین دلیل بصورت گسترده ای این افسانه در میان اهالی وب پذیرفته شده است.

حال بجای اینکه این موضوع را بارها و بارها در جاهای مختلف بیان کنیم، این مقاله را نوشته و در هر کجا که لازم باشد به آن ارجاع خواهیم داد. باید توجه کنید این حقیقت که زبان PHP یک زبان اصیل و قدرتمند است هیچ شکی در آن نیست اما اینکه بخواهیم بصورت مغرضانه و به این دلیل که ما از این زبان استفاده می‌کنیم، آنرا از هر لحاظ برتر از سایر زبانها بدانیم (کمی که نه) بسیار اغراق آمیز است.

این مقاله برای این نیست که ما هریک از این زبانها را زیر سوال ببریم. بلکه برای آن است که این موضوع را با دلایل منطقی و حقیقی بررسی کنیم که آیا اینکه می‌گویند PHP از ASP.net سریعتر است واقعیت دارد یا نه؟

Compiled در مقابل Interpreted Languages:

قبل از هرچیز ذکر این نکته الزامی است که این دو زبان تفاوتهای اساسی در base دارند. ASP.net یک زبان بهینه سازی و کامپایل شده است، به این معنی که کدهای نوشته شده در این زبان قبل از اینکه قابل اجرا شوند، به مجموعه ای از دستورالعمل‌های خاص ماشین تبدیل می‌شوند. از سوی دیگر PHP یک زبان تفسیر شده است، به این معنی که کدهای نوشته شده به همان شکل ذخیره شده و در زمان اجرا این کدها تفسیر می‌شوند. این موضوع بطور گسترده‌ای پذیرفته شده و ثابت شده است که برنامه‌های کامپایل شده به مراتب سریعتر از برنامه‌های تفسیر شده اجرا می‌شوند، به این دلیل که برنامه‌های تفسیر شده نیاز دارند تا در زمان اجرا به دستورالعملهای ماشین تبدیل شوند.

در اینجا به یک نقل قول از دانشنامه آزاد ویکی پدیا اشاره می‌کنم که میزان سریعتر بودن برنامه‌های کامپایل شده را نشان می‌دهد:

"A program translated by a compiler tends to be much faster than an interpreter executing the same program: even a 10:1 ratio is not uncommon. The mixed solution's efficiency is typically somewhere in between."

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

اعداد و ارقام:

حال که تئوری خود را مبنی بر دلیل سریعتر بودن ASP.net بیان کردیم بیایید با هم نگاهی به برخی آمارها بیاندازیم تا این تئوری را در عمل هم نشان داده باشیم.

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

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


همانطور که میبینید زمان متوسط برای سایت PHP، 0.1500 ثانیه و برای سایت ASP.net، 0.0150 ثانیه است. یک تفاوت بزرگ: PHP ده برابر نسبت به ASP.net کندتر است!

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


PHP، 1.0097 ثانیه طول می‌کشد در حالی که ASP.net، 0.0810 ثانیه زمان نیاز دارد. می‌بینیم که PHP دوازده بار بیشتر از ASP.net زمان می‌برد.

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

آمارهای زیر از سایت معتبر shootout.alioth.debian.org گرفته شده است. این آمارها نحوه اجرای همان کد را بر روی سیستم عامل لینوکسی برای هردو زبان نشان می‌دهد: 

 

همانطور که مشاهده می‌کنید در سیستم لینوکسی نیز همچنان ASP.net سریعتر از PHP عمل می‌کند.

نتیجه گیری:

همین حالا جمله‌ی "asp.net vs php speed" را در google جستجو کنید. خواهید دید که در اکثر پست‌ها گفته شده که PHP از ASP.net سریعتر است اما دلیلی بر این ادعا نخواهید یافت و فقط در حد حرف است. مشکل این است که اکثر مردم وقتی چیزی را زیاد می‌بینند یا زیاد می‌شنوند بدون آنکه دلیل بخواهند آنرا می‌پذیریند و حتی بعضی اوقات از آن نیز دفاع می‌کنند که واقعا جای تاسف دارد.

توسعه وب بوسیله PHP کار خوبی است، بسیاری از اپلیکیشن‌ها و وبسایتهای شگفت انگیز توسط این زبان نوشته شده اند. اگر احساس می‌کنید PHP یک زبان برتر است از آن استفاده کنید اما این دلیل نمی‌شود که اطلاعات غلط را به دیگران القاء کنید و بدون دلیل و مدرک این زبان را از هر لحاظ برتر بدانید حال آنکه در این مقاله دیدیم که براساس چیزی که ارائه شد، ASP.net سرعت بیشتری نسبت به PHP دارد.

اگر با من در این امر موافق نیستید می‌توانید با نظرهای مستدل خود ما را راهنمایی کنید.