مطالب
حذف تگ‌های زاید دریافتی از متون MS-Word

یکی از مشکلاتی که من همیشه با کاربران عادی دارم بحث انتقال مطالب از Word مایکروسافت به ادیتورهای WYSWING تحت وب است. برای مثال شما سایت پویایی را درست کرده‌اید که کاربران می‌توانند مطالب آنرا ویرایش یا کم و زیاد کنند.
اگر مطلب از ابتدا در این نوع ادیتورها تایپ و آماده شود هیچ مشکلی وجود نخواهد داشت چون خروجی اکثر آنها استاندارد است، اما متاسفانه خروجی وب word بسیار مشکل‌زا است (copy/paste معمولی مطالب آن در یک ادیتور تحت وب) و خصوصا برای نمایش تایپ فارسی در وب اصلا مناسب نیست. یعنی هیچ الزامی وجود ندارد که اندازه فونت‌ها در متن نهایی نمایش داده شده در وب یکسان باشند یا خطوط در هم فرو نروند و یا عدم تناسب اندازه قلم متن صفحه با قلم استفاده شده در CSS‌ سایت (که شکل ناهماهنگ و غیرحرفه‌ای را حاصل خواهد کرد) و امثال آن. اینجاست که کار شما زیر سؤال می‌رود! "این برنامه درست کار نمیکنه! متن من به‌هم ریخته شده و امثال این"
این کاربر عادی عموما یک تایپیست است یا یک منشی که به او گفته شده است شما از امروز موظفید مطالبی را در این سایت قرار دهید. بنابراین این کاربر حتما از word استفاده خواهد کرد (برای پیش نویس مطالب). همچنین عموما هم مرورگر "سازمانی" مورد استفاده، هنوز که هنوز است همان IE6 است (در اکثر شرکت‌ها و خصوصا ادارات) و مهم نیست که الان آخرین نگارش IE یا فایرفاکس و تمام هیاهوهای مربوطه به کجا ختم شده‌اند. حتما باید سایت با IE6 هم سازگار باشد. بنابراین از برنامه IE tester غافل نشوید.
و دست آخر شما هم نمی‌توانید به کاربر عادی ثابت کنید که این خروجی وب word اصلا استاندارد نیست (حتما کار شما است که مشکل دارد نه شرکت معظم مایکروسافت!). یا اینکه به آنها بگوئید اصلا مجاز نیستید در وب همانند یک فایل word از چندین نوع قلم مختلف فارسی غیراستاندارد استفاده کنید چون ممکن است کاربری این نوع قلم مورد استفاده شما را نداشته باشد و نمایش نهایی به هم ریخته‌تر از آنی خواهد بود که شما فکرش را می‌کنید! یا اینکه با استفاده از این روش حجم نهایی صفحه حداقل 50 کیلو بایت بیشتر خواهد شد (بدلیل حجم بالای تگ‌های زاید word) و نباید کاربران دایال آپ را فراموش کرد.
مدتی در اینباره جستجو کردم و نتیجه حاصل این بود که تمامی روش‌ها به یک مورد ختم می‌شود: حذف تگ‌های غیراستاندارد word هنگام دریافت مطلب و پیش از ذخیره سازی آن در دیتابیس
یک سری از ادیتورهای متنی تحت وب مانند FCK editor این قابلیت را به صورت خودکار اضافه کرده‌اند و حتی اگر کاربر متنی را از word در آنها Paste کند پیغامی را در همین رابطه دریافت خواهد کرد (شکل زیر) و البته کاربر می‌تواند گزینه لغو یا خیر را نیز انتخاب کند و دوباره همان وضعیت قبل تکرار خواهد شد. (یا حتی دکمه مخصوص کپی از word را هم به نوار ابزار خود اضافه کرده‌اند)



برای این منظور تابع زیر تهیه شده‌است که من همواره از آن استفاده می‌کنم و تا به امروز مشکل پاسخ پس دادن به کاربران عادی را به این صورت حل کرده‌ام!
این تابع تمامی تگ‌های اضافی و غیراستاندارد word متن دریافتی از یک ادیتور WYSWING را حذف می‌کند و به این صورت متن نهایی نمایش داده شده در سایت، تابع CSS مورد استفاده در سایت خواهد شد و نه حجم بالایی از تگ‌های غیراستاندارد word. (ممکن است کاربر در ابتدا کمی جا بخورد ولی مهم نیست! سایت باید استاندارد نمایشی خودش را از CSS آن دریافت کند و نه از تگ‌های word)

using System.Text.RegularExpressions;
/// <summary>
/// Removes all FONT and SPAN tags, and all Class and Style attributes.
/// Designed to get rid of non-standard Microsoft Word HTML tags.
/// </summary>
public static string CleanMSWordHtml(string html)
{
try
{
// start by completely removing all unwanted tags
html = Regex.Replace(html, @"<[/]?(font|span|xml|del|ins|[ovwxp]:\w )[^>]*?>", "", RegexOptions.IgnoreCase);
// then run another pass over the html (twice), removing unwanted attributes
html = Regex.Replace(html, @"<([^>]*)(?:class|lang|style|size|face|[ovwxp]:\w )=(?:'[^']*'|""[^""]*""|[^\s>] )([^>]*)>", "<$1$2>", RegexOptions.IgnoreCase);
html = Regex.Replace(html, @"<([^>]*)(?:class|lang|style|size|face|[ovwxp]:\w )=(?:'[^']*'|""[^""]*""|[^\s>] )([^>]*)>", "<$1$2>", RegexOptions.IgnoreCase);
return RemoveHTMLComments(html);
}
catch
{
return html;
}
}

public static string RemoveHTMLComments(string html)
{
try
{
Regex _Regex = new Regex("((<!-- )((?!<!-- ).)*( -->))(\r\n)*", RegexOptions.Singleline);
return _Regex.Replace(html, string.Empty);
}
catch
{
return html;
}
}

متد RemoveHTMLComments را عمدا جدا قرار دادم تا مشخص‌تر باشد. پس از تمیزکاری اولیه، ممکن است دسته‌گل‌های تیم مایکروسافت به صورت کامنت باقی بمانند که باید آنها را هم تمیز کرد! :)

مطالب
استفاده از Kendo UI templates
در مطلب «صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid» در انتهای بحث، ستون IsAvailable به صورت زیر تعریف شد:
columns: [
               {
                   field: "IsAvailable", title: "موجود است",
                   template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                }
]
Templates، جزو یکی از پایه‌های Kendo UI Framework هستند و توسط آن‌ها می‌توان قطعات با استفاده‌ی مجدد HTML ایی را طراحی کرد که قابلیت یکی شدن با اطلاعات جاوا اسکریپتی را دارند.
همانطور که در این مثال نیز مشاهده می‌کنید، قالب‌های Kendo UI از Hash (#) syntax استفاده می‌کنند. در اینجا قسمت‌هایی از قالب که با علامت # محصور می‌شوند، در حین اجرا، با اطلاعات فراهم شده جایگزین خواهند شد.
برای رندر مقادیر ساده می‌توان از # =# استفاده کرد. از # :# برای رندر اطلاعات HTML-encoded کمک گرفته می‌شود و #  # برای رندر کدهای جاوا اسکریپتی کاربرد دارد. از حالت HTML-encoded برای نمایش امن اطلاعات دریافتی از کاربران و جلوگیری از حملات XSS استفاده می‌شود.
اگر در این بین نیاز است # به صورت معمولی رندر شود، در حالت کدهای جاوا اسکریپتی به صورت #\\ و در HTML ساده به صورت #\ باید مشخص گردد.


مثالی از نحوه‌ی تعریف یک قالب Kendo UI

    <!--دریافت اطلاعات از منبع محلی-->
    <script id="javascriptTemplate" type="text/x-kendo-template">
        <ul>
            # for (var i = 0; i < data.length; i++) { #
            <li>#= data[i] #</li>
            # } #
        </ul>
    </script>

    <div id="container1"></div>
    <script type="text/javascript">
        $(function () {
            var data = ['User 1', 'User 2', 'User 3'];
            var template = kendo.template($("#javascriptTemplate").html());
            var result = template(data); //Execute the template
            $("#container1").html(result); //Append the result
        });
    </script>
این قالب ابتدا در تگ script محصور می‌شود و سپس نوع آن مساوی text/x-kendo-template قرار می‌گیرد. در ادامه توسط یک حلقه‌ی جاوا اسکریپتی، عناصر آرایه‌ی فرضی data خوانده شده و با کمک Hash syntax در محل‌های مشخص شده قرار می‌گیرند.
در ادامه باید این قالب را رندر کرد. برای این منظور یک div با id مساوی container1 را جهت تعیین محل رندر نهایی اطلاعات مشخص می‌کنیم. سپس متد kendo.template بر اساس id قالب اسکریپتی تعریف شده، یک شیء قالب را تهیه کرده و سپس با ارسال آرایه‌ای به آن، سبب اجرای آن می‌شود. خروجی نهایی، یک قطعه کد HTML است که در محل container1 درج خواهد شد.
همانطور که ملاحظه می‌کنید، متد kendo.template، نهایتا یک رشته را دریافت می‌کند. بنابراین همینجا و به صورت inline نیز می‌توان یک قالب را تعریف کرد.


کار با منابع داده راه دور

فرض کنید مدل برنامه به صورت ذیل تعریف شده‌است:
namespace KendoUI04.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}
و لیستی از آن توسط یک ASP.NET Web API کنترلر، به سمت کاربر ارسال می‌شود:
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using KendoUI04.Models;

namespace KendoUI04.Controllers
{
    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            return ProductDataSource.LatestProducts.Take(10);
        }
    }
}
در سمت کاربر و در View برنامه خواهیم داشت:
    <!--دریافت اطلاعات از سرور-->
    <div>
        <div id="container2"><ul></ul></div>
    </div>

    <script id="template1" type="text/x-kendo-template">
        <li> #=Id# - #:Name# - #=kendo.toString(Price, "c")#</li>
    </script>

    <script type="text/javascript">
        $(function () {
            var producatsTemplate1 = kendo.template($("#template1").html());

            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function () {
                    $("#container2 > ul").html(kendo.render(producatsTemplate1, this.view()));
                }
            });
            productsDataSource.read();
        });
    </script>
ابتدا یک div با id مساوی container2 جهت تعیین محل نهایی رندر قالب template1 در صفحه تعریف می‌شود.
هرچند خروجی دریافتی از سرور نهایتا یک آرایه از اشیاء Product است، اما در template1 اثری از حلقه‌ی جاوا اسکریپتی مشاهده نمی‌شود. در اینجا چون از متد kendo.render استفاده می‌شود، نیازی به ذکر حلقه نیست و به صورت خودکار، به تعداد عناصر آرایه دریافتی از سرور، قطعه HTML قالب را تکرار می‌کند.
در ادامه برای کار با سرور از یک Kendo UI DataSource استفاده شده‌است. قسمت transport/read آن، کار تعریف محل دریافت اطلاعات را از سرور مشخص می‌کند. رویدادگران change آن اطلاعات نهایی دریافتی را توسط متد view در اختیار متد kendo.render قرار می‌دهد. در نهایت، قطعه‌ی HTML رندر شده‌ی نهایی حاصل از اجرای قالب، در بین تگ‌های ul مربوط به container2 درج خواهد شد.
رویدادگران change زمانیکه data source، از اطلاعات راه دور و یا یک آرایه‌ی جاوا اسکریپتی پر می‌شود، فراخوانی خواهد شد. همچنین مباحث مرتب سازی اطلاعات، صفحه بندی و تغییر صفحه، افزودن، ویرایش و یا حذف اطلاعات نیز سبب فراخوانی آن می‌گردند. متد view ایی که در این مثال فراخوانی شد، صرفا در روال رویدادگردان change دارای اعتبار است و آخرین تغییرات اطلاعات و آیتم‌های موجود در data source را باز می‌گرداند.


یک نکته‌ی تکمیلی: فعال سازی intellisense کدهای جاوا اسکریپتی Kendo UI

اگر به پوشه‌ی اصلی مجموعه‌ی Kendo UI مراجعه کنید، یکی از آن‌ها vsdoc نام دارد که داخل آن فایل‌های min.intellisense.js و vsdoc.js مشهود هستند.
اگر از ویژوال استودیوهای قبل از 2012 استفاده می‌کنید، نیاز است فایل‌های vsdoc.js متناظری را به پروژه اضافه نمائید؛ دقیقا در کنار فایل‌های اصلی js موجود. اگر از ویژوال استودیوی 2012 و یا بالاتر استفاده می‌کنید باید از فایل‌های intellisense.js متناظر استفاده کنید. برای مثال اگر از kendo.all.min.js کمک می‌گیرید، فایل متناظر با آن kendo.all.min.intellisense.js خواهد بود.
بعد از اینکار نیاز است فایلی به نام references.js_ را به پوشه‌ی اسکریپت‌های خود با این محتوا اضافه کنید (برای VS 2012 به بعد):
/// <reference path="jquery.min.js" />
/// <reference path="kendo.all.min.js" />
نکته‌ی مهم اینجا است که این فایل به صورت پیش فرض از مسیر Scripts/_references.js/~ خوانده می‌شود. برای اضافه کردن مسیر دیگری مانند js/_references.js/~ باید آن‌را به تنظیمات ذیل اضافه کنید:
 Tools menu –> Options -> Text Editor –> JavaScript –> Intellisense –> References
گزینه‌ی Reference Group را به (Implicit (Web تغییر داده و سپس مسیر جدیدی را اضافه نمائید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI04.zip
نظرات اشتراک‌ها
به زودی: ارائه‌ی کرنل لینوکس به عنوان یک کامپوننت ویندوز
این مورد بازنویسی Windows Subsystem for Linux - WSL فعلی، جهت بالابردن کارآیی آن است. به این صورت برای اجرای Docker لینوکسی در ویندوز دیگر نیازی به ماشین مجازی فعلی آن نخواهد بود.
اشتراک‌ها
فریم ورک هوش مصنوعی و یادگیری ماشین

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

فریم ورک هوش مصنوعی و یادگیری ماشین
اشتراک‌ها
SVM یا ماشین بردار پشتیبان به زبان ساده

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

SVM یا ماشین بردار پشتیبان به زبان ساده
اشتراک‌ها
بهبود کیفیت گرافیکی ویندوز 8 در VMWare
پس از نصب ویندوز 8 در VMWare نیاز است ابتدا VMWare Tools نصب شود. سپس فایل vmx ماشین مجازی را یافته و یک سطر ذیل را به انتهای آن اضافه کنید:
svga.vramSize = "134217728"
بهبود کیفیت گرافیکی ویندوز 8 در VMWare
نظرات مطالب
مفاهیم برنامه نویسی ـ مروری بر کلاس و شیء
شروع خوبی بود، آفرین!
من هنوز وقتی جلسه‌ی اول کلاس میپرسم که پراید یک کلاس هست و یا یک آبجکت. همه میگن آبجکتی از کلاس ماشین!
این در حالیست که خیلی از این افراد سالهاست برنامه نویسی میکنند و هنوز نمیدونند heap یا stack چیه
نظرات مطالب
مرور چند تجربه کوتاه با Microsoft virtual pc
یک راه دیگر هم هست:
به settings مربوط به ماشین مجازی خود مراجعه کنید. یک قسمت آن shared folders نام دارد. در اینجا می‌شود یک فولدر روی ویندوز اصلی را با ویندوز مجازی به اشتراک گذاشت و فایل رد و بدل کرد.
مطالب
مسیریابی در Angular - قسمت سوم - پارامترهای مسیریابی
گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آن‌ها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحه‌ی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.

تنظیم مسیریابی‌ها جهت درج پارامترها

پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابی‌ها مشخص کرد.
در ادامه‌ی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه می‌کنیم:
>ng g c product/ProductDetail
>ng g c product/ProductEdit
این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.

در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آن‌را به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';
import { ProductListComponent } from './product-list/product-list.component';

const routes: Routes = [
  { path: 'products', component: ProductListComponent },
  { path: 'products/:id', component: ProductDetailComponent },
  { path: 'products/:id/edit', component: ProductEditComponent }
];
اولین شیء مسیریابی تعریف شده را در قسمت‌های پیشین بررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر می‌کند.
دومین شیء مسیریابی، از مسیر ریشه‌ی یکسانی استفاده می‌کند (products) که علت آن‌را در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص می‌کند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری می‌خواهیم id محصولات را انتقال دهیم، Id نام‌گرفته‌است؛ وگرنه هر نام دیگری نیز می‌تواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشه‌ی یکسانی استفاده می‌کند و تفاوت آن‌را با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کرده‌ایم.

در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نام‌های پس از : در یک مسیریابی، باید منحصربفرد باشند.


تکمیل کامپوننت نمایش لیست محصولات

پیش از ادامه‌ی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمت‌های قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینک‌هایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه می‌کنیم:
 > ng g i product/IProduct
و آن‌را به نحو ذیل تکمیل خواهیم کرد:
export interface IProduct {
    id: number;
    productName: string;
    productCode: string;
}


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

یکی از بسته‌های Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائه‌ی مطلب).
به همین جهت ابتدا بسته‌ی مرتبط با آن‌را نصب کنید:
 >npm install angular-in-memory-web-api --save
ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
"dependencies": {
   "angular-in-memory-web-api": "^0.3.1"
},

سپس کلاس ProductData را به ماژول محصولات اضافه می‌کنیم:
 > ng g cl product/ProductData
این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
import { IProduct } from './iproduct';
import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api';

export class ProductData implements InMemoryDbService, InMemoryBackendConfig {
    createDb() {
        let products: IProduct[] = [
            {
                'id': 1,
                'productName': 'Product 1',
                'productCode': '0011'
            },
            {
                'id': 2,
                'productName': 'Product 2',
                'productCode': '0023'
            },
            {
                'id': 5,
                'productName': 'Product 5',
                'productCode': '0048'
            },
            {
                'id': 8,
                'productName': 'Product 8',
                'productCode': '0022'
            },
            {
                'id': 10,
                'productName': 'Product 10',
                'productCode': '0042'
            }
        ];
        return { products };
    }
}
همانطور که ملاحظه می‌کنید، کلاسی که قرار است  به عنوان منبع داده‌ی بسته‌ی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونه‌ای از آن‌را در اینجا برای بازگشت یک لیست درون حافظه‌ای محصولات، مشاهده می‌کنید.

در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';

@NgModule({
  declarations: [
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }),

    ProductModule,
    UserModule,

    AppRoutingModule
  ],
- ابتدا مکان یافت شدن ماژول‌های مورد نیاز ProductData و InMemoryWebApiModule تعریف شده‌اند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائه‌ی اطلاعات کلاس ProductData، به لیست imports اضافه کرده‌ایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرض‌های آن را بازنویسی کند.
- در اینجا یک delay را هم در تنظیمات آن مشاهده می‌کنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.


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

پس از تعریف REST Web API درون حافظه‌ای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
 >ng g s product/product -m product/product.module
که سبب افزوده شدن سرویس product.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول product.module.ts نیز می‌شود:
 installing service
  create src\app\product\product.service.spec.ts
  create src\app\product\product.service.ts
  update src\app\product\product.module.ts
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمی‌شود.

پس از ایجاد قالب ابتدایی فایل product.service.ts، آن‌را به نحو ذیل تکمیل کنید:
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 { IProduct } from './iproduct';

@Injectable()
export class ProductService {
  private baseUrl = 'api/products';

  constructor(private http: Http) { }

  getProducts(): Observable<IProduct[]> {
    return this.http.get(this.baseUrl)
      .map(this.extractData)
      .do(data => console.log('getProducts: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  getProduct(id: number): Observable<IProduct> {
    if (id === 0) {
      return Observable.of(this.initializeProduct());
    };
    const url = `${this.baseUrl}/${id}`;
    return this.http.get(url)
      .map(this.extractData)
      .do(data => console.log('getProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  deleteProduct(id: number): Observable<Response> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    const url = `${this.baseUrl}/${id}`;
    return this.http.delete(url, options)
      .do(data => console.log('deleteProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }

  private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    product.id = undefined;
    return this.http.post(this.baseUrl, product, options)
      .map(this.extractData)
      .do(data => console.log('createProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> {
    const url = `${this.baseUrl}/${product.id}`;
    return this.http.put(url, product, options)
      .map(() => product)
      .do(data => console.log('updateProduct: ' + JSON.stringify(data)))
      .catch(this.handleError);
  }

  private extractData(response: Response) {
    let body = response.json();
    return body.data || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
  }

  initializeProduct(): IProduct {
    // Return an initialized object
    return {
      id: 0,
      productName: null,
      productCode: null
    };
  }
}
این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس  baseUrl، متصل خواهد شد:
   private baseUrl = 'api/products';
از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.


نمایش لیست محصولات

اکنون پس از این مقدمات می‌توانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آن‌را به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from '@angular/core';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  pageTitle = 'Product List';
  errorMessage: string;

  products: IProduct[];

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.productService.getProducts()
      .subscribe(products => this.products = products,
      error => this.errorMessage = <any>error);
  }
}
در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده می‌شود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آن‌را به نحو ذیل تکمیل کنید:
<div class="panel panel-default">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body">
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>

    <div class="table-responsive">
      <table class="table" *ngIf="products && products.length">
        <thead>
          <tr>
            <th>Product</th>
            <th>Code</th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let product of products">
            <td><a [routerLink]="['/products', product.id]">
                  {{product.productName}}
                </a>
            </td>
            <td>{{ product.productCode}}</td>
            <td>
              <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
                Edit
              </a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحه‌ای قابل مشاهده خواهد بود:


توضیحات:

پس از تعریف مسیریابی‌های صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آن‌ها به لینک‌هایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحه‌ی مشاهده‌ی جزئیات آن متصل کرده‌ایم:
<a [routerLink]="['/products', product.id]">
                  {{product.productName}}
</a>
برای مشاهده‌ی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب داده‌ایم.
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کرده‌ایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']">
     Edit
</a>
در اینجا ابتدا root URL segment ذکر می‌شود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit‌، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.

به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه می‌کنیم:
<li>
      <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
</li>
در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحه‌ی ویرایش اطلاعات یک محصول، ارسال کرده‌ایم. اگر به سرویس محصولات دقت کنید،
  saveProduct(product: IProduct): Observable<IProduct> {
    let headers = new Headers({ 'Content-Type': 'application/json' });
    let options = new RequestOptions({ headers: headers });

    if (product.id === 0) {
      return this.createProduct(product, options);
    }
    return this.updateProduct(product, options);
  }
اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی می‌گردد.

همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز می‌توان بکار برد. برای مثال:
 this.router.navigate(['products', this.product.id]);


خواندن و پردازش پارامترهای مسیریابی

تا اینجا لیست محصولات را نمایش دادیم و همچنین لینک‌هایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحله‌ی بعد، پیاده سازی این کامپوننت‌ها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننت‌ها قرار می‌دهد. بنابراین برای دسترسی به آن‌ها تنها کافی است این سرویس را به سازنده‌ی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم می‌شود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id می‌توان به صورت ذیل عمل کرد:
 let id = +this.route.snapshot.params['id'];

بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف می‌شود:
 { path: 'products/:id', component: ProductDetailComponent }
سپس این مسیریابی توسط لینک ذیل فعال می‌شود:
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
اکنون برای دریافت مقدار این پارامتر از URL جاری، می‌توان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شده‌است و حساس به کوچکی و بزرگی حروف می‌باشد.

ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز می‌باشد:
this.route.params.subscribe(
         params => {
            let id = +params['id'];
            this.getProduct(id);
         }
      );
هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آن‌ها گوش فرا داده و برنامه را از این تغییرات مطلع می‌سازد.

یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشته‌ی دریافتی، به عدد می‌گردد.


تکمیل کامپوننت نمایش جزئیات یک محصول

در ادامه قالب ابتدایی مشاهده‌ی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
  styleUrls: ['./product-detail.component.css']
})
export class ProductDetailComponent implements OnInit {
  pageTitle = 'Product Detail';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number) {
    this.productService.getProduct(id).subscribe(
      product => this.product = product,
      error => this.errorMessage = <any>error);
  }
}
در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آن‌را از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازنده‌ی کلاس کامپوننت جزئیات محصول تزریق شده‌است. هرچند می‌توان از این سرویس در همان سازنده‌ی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی می‌گردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده می‌کنید.
در متد OnInit، مقدار id، از snapshot دریافت می‌گردد. سپس این id به متد getProduct ارسال می‌شود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.


برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product">
  <div class="panel-heading" style="font-size:large">
    {{pageTitle + ": " + product.productName}}
  </div>

  <div class="panel-body">
    <div>
      Name: {{product.productName}}
    </div>
    <div>
      Code: {{product.productCode}}
    </div>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>

  <div class="panel-footer">
    <a class="btn btn-default" [routerLink]="['/products']">
      <i class="glyphicon glyphicon-chevron-left"></i> Back
    </a>
    <a class="btn btn-primary" style="width:80px;margin-left:10px" 
       [routerLink]="['/products', product.id, 'edit']">
       Edit
    </a>
  </div>
</div>
در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمه‌ی back و edit نیز اضافه شده‌اند که اولی صفحه‌ی لیست محصولات را مجددا نمایش می‌دهد و دومی کار هدایت به صفحه‌ی ویرایش جزئیات این محصول را میسر می‌کند.


تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول

از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    ProductRoutingModule
  ]
کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';

import { ProductService } from './../product.service';
import { IProduct } from './../iproduct';

@Component({
  selector: 'app-product-edit',
  templateUrl: './product-edit.component.html',
  styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit {
  pageTitle = 'Product Edit';
  product: IProduct;
  errorMessage: string;

  constructor(private productService: ProductService,
    private route: ActivatedRoute,
    private router: Router) { }

  ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }

  getProduct(id: number): void {
    this.productService.getProduct(id)
      .subscribe(
      (product: IProduct) => this.onProductRetrieved(product),
      (error: any) => this.errorMessage = <any>error
      );
  }

  onProductRetrieved(product: IProduct): void {
    this.product = product;

    if (this.product.id === 0) {
      this.pageTitle = 'Add Product';
    } else {
      this.pageTitle = `Edit Product: ${this.product.productName}`;
    }
  }

  deleteProduct(): void {
    if (this.product.id === 0) {
      // Don't delete, it was never saved.
      this.onSaveComplete();
    } else {
      if (confirm(`Really delete the product: ${this.product.productName}?`)) {
        this.productService.deleteProduct(this.product.id)
          .subscribe(
          () => this.onSaveComplete(`${this.product.productName} was deleted`),
          (error: any) => this.errorMessage = <any>error
          );
      }
    }
  }

  saveProduct(): void {
    if (true === true) {
      this.productService.saveProduct(this.product)
        .subscribe(
        () => this.onSaveComplete(`${this.product.productName} was saved`),
        (error: any) => this.errorMessage = <any>error
        );
    } else {
      this.errorMessage = 'Please correct the validation errors.';
    }
  }

  onSaveComplete(message?: string): void {
    if (message) {
      // TODO: show msg
    }

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }
}
به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
<div class="panel panel-primary">
  <div class="panel-heading">
    {{pageTitle}}
  </div>

  <div class="panel-body" *ngIf="product">
    <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm">
      <fieldset>
        <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || 
                                               productNameVar.dirty) && 
                                               !productNameVar.valid }">
          <label class="col-md-2 control-label" for="productNameId">Product Name</label>

          <div class="col-md-8">
            <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" 
                   required minlength="3" [(ngModel)]=product.productName 
                   name="productName" #productNameVar="ngModel" />
            <span class="help-block" *ngIf="(productNameVar.touched ||
                                                         productNameVar.dirty) &&
                                                         productNameVar.errors">
                <span *ngIf="productNameVar.errors.required">
                    Product name is required.
                </span>
                <span *ngIf="productNameVar.errors.minlength">
                    Product name must be at least three characters.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || 
                                               productCodeVar.dirty) && 
                                               !productCodeVar.valid }">
          <label class="col-md-2 control-label" for="productCodeId">Product Code</label>

          <div class="col-md-8">
            <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" 
                   required [(ngModel)]=product.productCode
                   name="productCode" #productCodeVar="ngModel" />
            <span class="help-block" *ngIf="(productCodeVar.touched ||
                                                         productCodeVar.dirty) &&
                                                         productCodeVar.errors">
                <span *ngIf="productCodeVar.errors.required">
                     Product code is required.
                </span>
            </span>
          </div>
        </div>

        <div class="form-group">
          <div class="col-md-4 col-md-offset-2">
            <span>
                 <button class="btn btn-primary"
                         type="submit"
                         style="width:80px;margin-right:10px"
                         [disabled]="!productForm.valid"
                         (click)="saveProduct()">
                         Save
                 </button>
             </span>
             <span>
                 <a class="btn btn-default"
                    [routerLink]="['/products']">
                       Cancel
                 </a>
             </span>
             <span>
                 <a class="btn btn-default"
                    (click)="deleteProduct()">
                     Delete
                  </a>
             </span>
          </div>
        </div>
      </fieldset>
    </form>
    <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div>
  </div>
</div>

توضیحات:

نکته‌ی مهمی را که در این کدها می‌خواهیم بررسی کنیم، متد ngOnInit آن‌است:
ngOnInit(): void {
    let id = +this.route.snapshot.params['id'];
    this.getProduct(id);
  }
برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
 - ابتدا لیست محصولات را مشاهده کنید.
 - سپس بر روی دکمه‌ی edit محصول شماره یک کلیک نمائید:


تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شده‌است. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیده‌است.
 - در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:


همانطور که مشاهده می‌کنید، هرچند URL صفحه تغییر کرده‌است، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشده‌اند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کرده‌ایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمی‌شود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر  root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمی‌شود. به همین جهت است که فیلدهای این فرم تغییر نکرده‌اند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آن‌ها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe می‌توان انجام داد:
  ngOnInit(): void {
    this.route.params.subscribe(
      params => {
        let id = +params['id'];
        this.getProduct(id);
      }
    );
  }
در اینجا چون کامپوننت به علت نحوه‌ی تعریف مسیریابی آن مجددا آغاز نمی‌شود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کرده‌ی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا می‌دهد، استفاده کرد.

یک نکته: هر زمانیکه از Observable‌ها استفاده می‌شود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت می‌شود.


روش خواندن پارامترهای مسیریابی در +Angular 4

روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کرده‌اند که توسط شیء paramMap مدیریت می‌شود:
    // For Angular 4+
    let id = +this.route.snapshot.paramMap.get('id');
    this.getProduct(id);

    // OR
    this.route.paramMap.subscribe(params => {
          let id = +params['id'];
          this.getProduct(id);
        });
در اینجا دو روش دسترسی به پارامتر id را مشاهده می‌کنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول می‌کند و یا بجای params می‌توان از paramMap استفاده کرد.


تعریف پارامترهای اختیاری مسیریابی

فرض کنید یک صفحه‌ی جستجو را طراحی کرده‌اید که در آن می‌توان نام و کد محصول را جستجو کرد. سپس می‌خواهیم این پارامترها را به صفحه‌ی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی می‌توان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
 { path: 'products/:name/:code', component: ProductListComponent }
و سپس می‌توان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
 [routerLink]="['/products', productName, productCode]"
این روش به همراه URLهایی ناخوانا خواهد بود که قسمت‌های مختلف آن مشخص نیستند و هر بار که قرار باشد گزینه‌ی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شده‌است. دراین حالت تعریف مسیریابی صفحه‌ی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
 { path: 'products', component: ProductListComponent },
و سپس لینکی که به آن تعریف می‌شود، نحوه‌ی تعریف خاصی را خواهد داشت:
 [routerLink]="['/products', {name: productName, code: productCode}]"
در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایه‌ی پارامترهای مسیریابی مشخص می‌شوند و هربار که نیاز به تغییر آن‌ها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
 http://localhost:4200/products;name=Controller;code=gmg
نحوه‌ی خواندن این پارامترها، دقیقا همانند نحوه‌ی خواندن پارامترهای اجباری هستند و در اینجا از نام key‌ها برای اشاره‌ی به آن‌ها استفاده می‌شود:
let name = this.route.snapshot.params['name'];
let code = this.route.snapshot.params['code'];

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


تعریف پارامترهای کوئری در مسیریابی

فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کرده‌اید. اکنون اگر بر روی لینک مشاهده‌ی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده می‌شود. در یک چنین حالتی برای بهبود تجربه‌ی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمت‌های مختلف حفظ کرد:
 http://localhost:42000/products/5?filterBy=product1
در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شده‌اند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
 { path: 'products', component: ProductListComponent}
اعمال پارامترهای کوئری مختلف، به لینک‌های تعریف شده، توسط دایرکتیو queryParams صورت می‌گیرد و در اینجا یک مجموعه‌ی از key/valueها ذکر خواهند شد:
<a [routerLink] = "['/products', product.id]"
     [queryParams] = "{ filterBy: 'er', showImage: true }">
{{product.productName}}
</a>
در این مثال یک ثابت دلخواه er مشخص شده‌است. بدیهی است می‌توان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شده‌ی در سطح کامپوننت که به تکست‌باکس جستجو متصل است).

و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'],
   {
       queryParams: { filterBy: 'er', showImage: true}
   }
);
باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ می‌شوند، نباید کلیدهای انتخاب شده‌ی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.

یک مشکل: اگر در صفحه‌ی نمایش جزئیات یک محصول، دکمه‌ی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمی‌شوند. مرحله‌ی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
 [preserveQueryParams] = "true"
یعنی دکمه‌ی back به این شکل تغییر می‌کند:
<a class="btn btn-default"
           [routerLink]="['/products']"
           [preserveQueryParams]="true">
            <i class="glyphicon glyphicon-chevron-left"></i> Back
</a>
و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافته‌است و حالت قبلی منسوخ شده اعلام گردیده‌است.
this.router.navigate(['/products'],
   { queryParamsHandling: 'preserve'}
);

پس از بازگشت به صفحه‌ی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شده‌اند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آن‌ها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || '';
var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده می‌شود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشته‌ای هستند و نه boolean. به همین جهت باید با رشته‌ی true مقایسه شوند.

در مثال تکمیل شده‌ی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شده‌است:
 >ng g p product/ProductFilter
فایل src\app\product\product-filter.pipe.ts با این محتوا:
import { PipeTransform, Pipe } from '@angular/core';
import { IProduct } from './iproduct';

@Pipe({
  name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform {
  transform(value: IProduct[], filterBy: string): IProduct[] {
    filterBy = filterBy ? filterBy.toLocaleLowerCase() : null;
    return filterBy ? value.filter((product: IProduct) =>
      product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value;
  }
}
و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\product-list.component.html :
  <div class="panel-body">
    <div class="row">
            <div class="col-md-2">Filter by:</div>
            <div class="col-md-4">
                <input type="text" [(ngModel)]="listFilter" />
            </div>
    </div>
    <div class="row" *ngIf="listFilter">
            <div class="col-md-6">
                <h3>Filtered by: {{listFilter}} </h3>
            </div>
    </div>
و اعمال این فیلتر به حلقه‌ی نمایش ردیف‌های جدول؛ به همراه تعریف پارامتر کوئری:
<tr *ngFor="let product of products | productFilter:listFilter">
            <td><a [routerLink]="['/products', product.id]"
                   [queryParams]="{filterBy: listFilter}">
                  {{product.productName}}
                </a>
            </td>


در اینجا اگر به صفحه‌ی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:


و در این حالت اگر بر روی دکمه‌ی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده می‌شود:


که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت می‌شود:
@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.css']
})
export class ProductListComponent implements OnInit {
  listFilter: string;

  ngOnInit(): void {
    this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
AngularJS #3
در این مقاله مفاهیم انقیاد داده (Data Binding)، تزریق وابستگی (Dependency Injection)،هدایت گر‌ها (Directives) و سرویس‌ها را بررسی خواهیم کرد و از مقاله‌ی آینده، به بررسی ویژگی‌ها و امکانات AngularJS در قالب مثال خواهیم پرداخت.
 
انقیاد داده (Data Binding)
سناریو هایی وجود دارد که در آن‌ها باید اطلاعات قسمتی از صفحه به صورت نامتقارن (Asynchronous) با داده‌های دریافتی جدید به روز رسانی شود. روش معمول برای انجام چنین کاری؛ دریافت داده‌ها از سرور است که عموما به فرم HTML میباشند و جایگزینی آن با بخشی از صفحه که قرار است به روز رسانی شود، اما حالتی را در نظر بگیرید که با داده هایی از جنس JSON طرف هستید و اطلاعات صفحه را با این داده‌ها باید به روز رسانی کنید. معمولا برای حل چنین مشکلی مجبور به نوشتن مقدار زیادی کد هستید تا بتوانید به خوبی اطلاعات View را به روز رسانی کنید. حتما با خودتان فکر کرده اید که قطعا راهی وجود دارد تا بدون نوشتن کدی، قسمتی از View را به Model متناظر خود نگاشت کرده و این دو به صورت بلادرنگ از تغییرات یکدیگر آگاه شوند. این عمل عموما به مفهوم انقیاد داده شناخته می‌شود و Angular هم به خوبی از انقیاد داده دوطرفه پشتیبانی می‌کند.
برای مشاهده این ویژگی در Angular، مثال مقاله‌ی قبل را به کد‌های زیر تغییر دهید تا پیغام به صورت پویا توسط کاربر وارد شود:
<!DOCTYPE html>
<html ng-app>
<head>
    <title>Sample2</title>
</head>
<body>
    <div>
        <input type="text" ng-model="greeting.text" />
        <p>{{greeting.text}}, World!</p>
    </div>
    <script src="../Scripts/angular.js"></script>
</body>
</html>
بدون نیاز به حتی یک خط کد نویسی! با مشخص کردن input به عنوان Model از طریق ng-model، خاصیت greeting.text که در داخل {{ }} مشخص شده را به متن داخل textbox  مقید (bind) کردیم.  نتیجه می‌گیریم که جفت آکلود {{ }} برای اعمال Data Binding استفاده می‌شود.
حال یک دکمه نیز بر روی فرم قرار می‌دهیم که با کلیک کردن بر روی آن، متن داخل textbox را نمایش دهد.
<!DOCTYPE html>
<html ng-app>
<head>
    <title>Sample2</title>
</head>
<body>
    <div ng-controller="GreetingController">
        <input type="text" ng-model="greeting.text" />
        <p>{{greeting.text}}, World!</p>
        <button ng-click="showData()">Show</button>
    </div>
    <script src="../Scripts/angular.js"></script>
    <script>
        var GreetingController = function ($scope, $window) {
            $scope.greeting = {
                text: "Hello"
            };

            $scope.showData = function () {
                $window.alert($scope.greeting.text);
            };
        };
    </script>
</body>
</html>
به کمک ng-click، تابع showData به هنگام کلیک شدن، فراخوانی می‌شود. window$ نیز به عنوان پارامتر کلاس GreetingController مشخص شده است. window$ نیز یکی از سرویس‌های پیش فرض تعریف شده توسط Angular است و ما در اینجا در سازنده‌ی کلاس آن را به عنوان وابستگی درخواست کرده ایم تا توسط سیستم تزریق وابستگی توکار، نمونه‌ی مناسب آن در اختیار ما بگذارد. window$ نیز تقریبا معادل شی window است و یکی از دلایل استفاده از آن ساده‌تر شدن نوشتن آزمون‌های واحداست.
حال متنی را داخل textbox نوشته  و دکمه‌ی show را فشار دهید. متن نوشته شده را به صورت یک popup  مشاهده خواهید کرد.
همچنین شی scope$ نیز نمونه‌ی مناسب آن توسط سیستم تزریق وابستگی Angular، در اختیار Controller قرار می‌گیرد و نمونه‌ی در اختیار قرارگرفته، برای ارتباط با View Model و سیستم انقیاد داده استفاده می‌شود.
معمولا انقیاد داده در الگوی طراحی (ModelView-ViewModel(MVVM مطرح است و به این دلیل که این الگوی طراحی به خوبی با الگوی طراحی MVC سازگار است، این امکان در Angular گنجانده شده است. 
   
تزریق وابستگی (Dependency Injection)
تا به این جای کار قطعن  بار‌ها و بار‌ها اسم آن را خوانده اید. در مثال فوق، پارامتری با نام scope$ را برای سازنده‌ی کنترلر خود در نظر گرفتیم و ما بدون انجام هیچ کاری نمونه‌ی مناسب آن را که برای انجام اعمال انقیاد داده با viewmodel استفاده می‌شود را دریافت کردیم. به عنوان مثال، window$ را نیز در سازنده‌ی کلاس کنترلر خود به عنوان یک وابستگی تعریف کردیم و تزریق نمونه‌ی مناسب آن توسط سیستم تزریق وابستگی توکار Angular صورت می‌گرفت.
اگر با IOC Container‌ها در زبانی مثل #C کار کرده باشید، قطعا با IOC Container فراهم شده توسط Angular هم مشکلی نخواهید داشت.
اما یک مشکل! در زبانی مثل #C که همه‌ی متغیر‌های دارای نوع هستند، IOC Container با استفاده از Reflection، نوع پارامترهای درخواستی توسط سازنده‌ی کلاس را بررسی کرده و با توجه به اطلاعاتی که ما از قبل در دسترس آن قرار داده بودیم، نمونه‌ی مناسب آن را در اختیار در خواست کننده می‌گذارد.
اما در زبان جاوا اسکریپت که متغیر‌ها دارای نوع نیستند، این کار به چه شکل انجام می‌گیرد؟
Angular برای این کار از نام پارامتر‌ها استفاده می‌کند. برای مثال Angular از نام پارامتر scope$ می‌فهمد که باید چه نمونه ای را به کلاس تزریق کند. پس نام پارامتر‌ها در سیستم تزریق وابستگی Angular نقش مهمی را ایفا می‌کنند.
اما در زبان جاوا اسکریپت، به طور پیش فرض امکانی برای به دست آوردن نام پارامتر‌های یک تابع وجود ندارد؛ پس Angular چگونه نام پارامتر‌ها را به دست می‌آورد؟ جواب در سورس کد Angular و در تابعی به نام annotate نهفته است که اساس کار این تابع استفاده از چهار عبارت با قاعده (Regular Expression) زیر است.
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
تابع annotate تابعی را به عنوان پارامتر دریافت می‌کند و سپس با فراخواندن متد toString آن، کدهای آن تابع را به شکل یک رشته در می‌آورد. حال کدهای تابع را که اکنون به شکل یک رشته در دسترس است را با استفاده از عبارات با قاعده‌ی فوق پردازش می‌کند تا نام پارامتر‌ها را به دست آورد. در ابتدا کامنت‌های موجود در تابع را حذف می‌کند، سپس نام پارامتر‌ها را استخراج می‌کند و با استفاده از "," آن‌ها را جدا می‌کند و در نهایت نام پارامتر‌ها را در یک آرایه باز می‌گرداند.
استفاده از تزریق وابستگی، امکان نوشتن کدهایی با قابلیت استفاده مجدد و نوشتن ساده‌تر آزمون‌های واحد را فراهم می‌کند. به خصوص کدهایی که با سرور ارتباط برقرار می‌کنند را می‌توان به یک سرویس انتقال داد و از طریق تزریق وابستگی، از آن در کنترلر استفاده کرد. سپس در آزمون‌های واحد می‌توان قسمت ارتباط با سرور را با یک نمونه فرضی جایگزین کرد تا برای تست، احتیاجی به راه اندازی یک وب سرور واقعی و یا مرورگر نباشد.
    
Directives
یکی از مزیت‌های Angular این است که قالب‌ها را می‌توان با HTML نوشت و این را باید مدیون موتور قدرتمند تبدیل گر DOM بدانیم  که در آن گنجانده شده است و به شما این امکان را می‌دهد تا گرامر HTML را گسترش دهید.
تا به این جای کار با attribute‌های زیادی در قالب HTML روبرو شدید که متعلق به HTML نیست. به طور مثال: جفت آکولاد‌ها که برای انقیاد داده به کار برده می‌شود، ng-app که برای مشخص کردن بخشی که باید توسط Angular کامپایل شود، ng-controller که برای مشخص کردن این که کدام بخش از View متعلق به کدام Controller است و ... تمامی Directive‌های پیش فرض Angular هستند.
با استفاده از Directive‌ها می‌توانید عناصر و خاصیت‌ها و حتی رویداد‌های سفارشی برای HTML بنویسید؛ اما واقعا چه احتیاجی به تعریف عنصر سفارشی و توسعه گرامر HTML وجود دارد؟
HTML یک زبان طراحی است که در ابتدا برای تولید اسناد ایستا به وجود آمد و هیچ وقت هدفش تولید وب سایت‌های امروزی که کاملا پویا هستند نبود. این امر تا جایی پیش رفته است که HTML را از یک زبان طراحی تبدیل به یک زبان برنامه نویسی کرده است و احتیاج به چنین زبانی کاملا مشهود است. به همین دلیل جامعه‌ی وب مفهومی را به نام Web Components  مطرح کرده است. Web Components به شما امکان تعریف عناصر HTML سفارشی را می‌دهد. برای مثال شما یک تگ سفارشی به نام datepicker می‌نویسید که دارای رفتار و ویژگی‌های خاص خود است و به راحتی عناصر HTML رابا استفاده از آن توسعه می‌دهید. مطمئنا آینده‌ی وب این گونه است، اما هنوز خیلی از مرورگرها از این ویژگی پشتیبانی نمی‌کنند.
یکی دیگر از معادل‌های  Web Component‌های امروز را می‌توان ویجت‌های jQuery UI دانست. اگر بخواهم تعریفی از ویجت ارائه دهم به این گونه است که یک ویجت؛ کدهای HTML، CSS و javascript مرتبط به هم را کپسوله کرده است. مهم‌ترین مزیت ویجت ها، قابلیت استفاده‌ی مجدد آن‌هاست، به این دلیل که تمام منطق مورد نیاز را در خود کپسوله کرده است؛ برای مثال ویجت datepicker که به راحتی در برنامه‌های مختلف بدون احتیاج به نوشتن کدی قابل استفاده است.
خب، متاسفانه Web Component‌ها هنوز در دنیای وب امروزی رایج نشده اند و ویجت‌ها هم آنچنان  قدرت Web Component‌ها را ندارند. خب Angular با استفاده از امکان تعریف Directive‌های سفارشی به صورت cross-browser امکان تعریف عناصر سفارشیه همانند web Component‌ها را به شما می‌دهد. حتی به عقیده‌ی عده ای Directive‌ها بسیار قدرتمند‌تر از Web Components عمل می‌کنند و راحتی کار با آن‌ها بیشتر است.
با استفاده از Directive‌ها می‌توانید عنصر HTML سفارشی مثل </ datepicker>،  خاصیت سفارشی مثل ng-controller، رویداد سفارشی مثل ng-click را  تعریف کنید و یا حتی حالت و اتفاقات رخ داده در برنامه را زیر نظر بگیرید.
و این یکی از دلایلی است که می‌گویند Angular دارای ویژگی forward-thinking است.
البته Directive‌ها یکی از قدرتمند‌ترین امکانات فریم ورک AngularJS است و در آینده به صورت مفصل بر روی آن بحث خواهد شد.
    
سرویس‌ها در AngularJS
 حتما این جمله را در هنگام نوشتن برنامه‌ها با الگوی طراحی MVC بار‌ها و بار‌ها شنیده اید که در Controller‌ها نباید منطق تجاری و پیچیده ای را پیاده سازی کرد و باید به قسمت‌های دیگری به نام سرویس‌ها منتقل شوند و سپس در سازنده‌ی کلاس کنترلر به عنوان پارامتر تعریف شوند تا توسط Angular نمونه‌ی مناسب آن به کنترلر تزریق شود. Controller‌ها نباید پیاده کننده‌ی هیچ منطق تجاری و یا اصطلاحا business برنامه باشد و باید از لایه‌ی سرویس استفاده کنند و تنها وظیفه‌ی کنترلر باید مشخص کردن انقیاد داده و حالت برنامه باشد.
دلیل استفاده از سرویس‌ها در کنترلر ها، نوشتن ساده‌تر آزمون‌های واحد و استفاده‌ی مجدد از سرویس‌ها در قسمت‌های مختلف پروژه و یا حتی پروژه‌های دیگر است.
معمولا اعمال مرتبط در ارتباط با سرور را در سرویس‌ها پیاده سازی می‌کنند تا بتوان در موقع نوشتن آزمون‌های واحد یک نمونه‌ی فرضی را خودمان ساخته و آن را به عنوان وابستگی به کنترلری که در حال تست آن هستیم تزریق کنیم، در غیر این صورت احتیاج به راه اندازی یک وب سرور واقعی برای نوشتن آزمون‌های واحد و در نتیجه کند شدن انجام آزمون را در بر دارد. قابلیت استفاده‌ی مجدد سرویس هم به این معناست که منطق پیاده سازی شده در آن نباید ربطی به رابط کاربری و ... داشته باشد. برای مثال یک سرویس به نام userService باید دارای متد هایی مثل دریافت لیست کاربران، افزودن کاربر و ... باشد و بدیهی است که از این سرویس‌ها می‌شود در قسمت‌های مختلف برنامه استفاده کرد. همچنین سرویس‌ها در Angular به صورت Singleton در اختیار کنترلر‌ها قرار می‌گیرند  و این بدین معناست که یک نمونه از هر سرویس ایجاد شده و به بخش‌های مختلف برنامه تزریق می‌شود. 
    
مفاهیم پایه ای AngularJs به پایان رسید. در مقاله بعدی یک مثال تقریبا کامل را نوشته و با اجزای مختلف Angular بیشتر آشنا می‌شویم.
   
با تشکر از مهدی محزونی برای بازبینی مطلب