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

در کتابخانه‌ی Microsoft AspNetCore Identity میتوان با این کد، فیلد Email را منحصر به‌فرد کرد:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

برنامه را اجرا و درخواست‌ها را یکی یکی به سمت سرور ارسال میکنیم و اگر ایمیل تکراری باشد به ما خطا میده و میگه: "ایمیل تکراری است".

ولی مشکل اینجاست که کد بالا فیلد Email رو داخل دیتابیس منحصر به‌فرد نمیکنه و فقط از سمت نرم افزار بررسی تکراری بودن ایمیل رو انجام میده. حالا اگه ما با استفاده از نرم افزارهای "تست برنامه‌های وب" مثل Apache JMeter تعداد زیادی درخواست را به سمت برنامه‌مان ارسال کنیم و بعد رکوردهای داخل جدول کاربران را نگاه کنیم، با وجود اینکه داخل نرم افزارمان پراپرتی Email را منحصر به‌فرد کرده‌ایم، ولی چندین رکورد، با یک ایمیل مشابه در داخل جدول User وجود خواهد داشت.

برای تست این سناریو، برنامه Apache JMeter را از این لینک دانلود می‌کنیم (در بخش Binaries فایل zip رو دانلود می کنیم).

نکته: داشتن jdk ورژن 8 به بالا پیش نیاز است. برای اینکه بدونید ورژن جاوای سیستمتون چنده، داخل cmd دستور java -version رو صادر کنید.

اگه تمایل به نصب، یا به روز رسانی jdk را داشتید، میتونید از این لینک استفاده کنید و بسته به سیستم عاملتون، یکی از تب‌های Windows, macOS یا Linux رو انتخاب کنید و فایل مورد نظر رو دانلود کنید (برای Windows فایل x64 Compressed Archive رو دانلود و نصب میکنیم).

حالا فایل دانلود شده JMeter رو استخراج میکنیم، وارد پوشه‌ی bin میشیم و فایل jmeter.bat رو اجرا میکنیم تا برنامه‌ی JMeter اجرا بشه.

قبل از اینکه وارد برنامه JMeter بشیم، کدهای برنامه رو بررسی می‌کنیم.

موجودیت کاربر:

public class User : IdentityUser<int>;

ویوو مدل ساخت کاربر:

public class UserViewModel
{
    public string UserName { get; set; } = null!;

    public string Email { get; set; } = null!;

    public string Password { get; set; } = null!;
}

کنترلر ساخت کاربر:

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };
        var result = await userManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return Ok();
        }
        return BadRequest(result.Errors);
    }
}

حالا وارد برنامه JMeter میشیم و اولین کاری که باید انجام بدیم این است که مشخص کنیم چند درخواست را در چند ثانیه قرار است ارسال کنیم. برای اینکار در برنامه JMeter روی TestPlan کلیک راست میکنیم و بعد:

Add -> Threads (Users) -> Thread Group

حالا باید بر روی Thread Group کلیک کنیم و بعد در بخش Number of threads (users) تعداد درخواست‌هایی را که قرار است به سمت سرور ارسال کنیم، مشخص کنیم؛ برای مثال عدد 100.

گزینه Ramp-up period (seconds) برای اینه که مشخص کنیم این 100 درخواست قرار است در چند ثانیه ارسال شوند که آن را روی 0.1 ثانیه قرار می‌دهیم تا درخواست‌ها را با سرعت بسیار زیاد ارسال کند.

الان باید مشخص کنیم چه دیتایی قرار است به سمت سرور ارسال شود:

برای اینکار باید یک Http Request اضافه کنیم. برای این منظور روی Thread Group که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Sampler -> Http Request

حالا روی Http Request کلیک میکنیم و متد ارسال درخواست رو که روی Get هست، به Post تغییر میدیم و بعد Path رو هم به آدرسی که قراره دیتا رو بهش ارسال کنیم، تغییر میدهیم:

https://localhost:7091/api/User

حالا پایینتر Body Data رو انتخاب میکنیم و دیتایی رو که قراره به سمت سرور ارسال کنیم، در قالب Json وارد میکنیم:

{
  "UserName": "payam${__Random(1000, 9999999)}",
  "Email": "payam@gmail.com",
  "Password": "123456aA@"
}

چون بخش UserName در پایگاه داده منحصر به‌فرد است، با این دستور:

${__Random(1000, 9999999)}

یک عدد Random رو به UserName اضافه میکنیم که دچار خطا نشیم.

حالا فقط باید یک Header رو هم به درخواستمون اضافه کنیم، برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Config Element -> Http Header Manager

حالا روی دکمه‌ی Add در پایین صفحه کلیک میکنیم و این Header رو اضافه میکنیم:

Name: Content-Type
Value: application/json

همچنین میتونیم یک View result رو هم اضافه کنیم تا وضعیت تمامی درخواست‌های ارسال شده رو مشاهده کنیم. برای اینکار روی Http Request که از قبل ایجاد کردیم، کلیک راست میکنیم و بعد:

Add -> Listener -> View Results Tree

فایل Backup، برای اینکه مراحل بالا رو سریعتر انجام بدید:

File -> Open

حالا بر روی دکمه‌ی سبز رنگ Play در Toolbar بالا کلیک میکنیم تا تمامی درخواست ها را به سمت سرور ارسال کنه و همچنین میتونیم از طریق View result tree ببینیم که چند درخواست موفقیت آمیز و چند درخواست ناموفق انجام شده‌است.

حالا اگر وارد پایگاه داده بشیم، میبینیم که چندین رکورد، با Email یکسان، در جدول User وجود داره:

در حالیکه ایمیل رو در تنظیمات کتابخانه Microsoft AspNetCore Identity به صورت Unique تعریف کرده‌ایم:

//Program.cs file
builder.Services.AddIdentity<User, Role>(options =>
{
    options.User.RequireUniqueEmail = true;
}).AddEntityFrameworkStores<DatabaseContext>();

دلیل این مشکل این است که درخواست‌ها در قالب یک صف، یک به یک اجرا نمیشوند؛ بلکه به صورت همزمان فریم ورک ASP.NET Core برای بالا بردن سرعت اجرای درخواست‌ها از تمامی Thread هایی که در اختیارش هست استفاده می‌کند و در چندین Thread جداگانه، درخواست‌هایی رو به کنترلر User میفرسته و در نتیجه، در یک زمان مشابه، چندین درخواست ارسال میشه که آیا یک ایمیل برای مثال با مقدار payam@yahoo.com وجود داره یا خیر و در تمامی درخواست‌ها چون همزمان انجام شده، جواب خیر است. یعنی ایمیل تکراری با آن مقدار، در پایگاه داده وجود ندارد و تمامی درخواست‌هایی که همزمان به سرور رسیده‌اند، کاربر جدید را با ایمیل مشابهی ایجاد می‌کنند.

این مشکل را میتوان حتی در سایت‌های فروش بلیط نیز پیدا کرد؛ یعنی چند نفر یک صندلی را رزرو کرده‌اند و همزمان وارد درگاه پرداخت شده و هزینه‌ایی را برای آن پرداخت میکنند. اگر آن درخواست‌ها را وارد صف نکنیم، امکان دارد که یک صندلی را به چند نفر بفروشیم. این سناریو برای زمانی است که در پایگاه داده، فیلد‌ها را Unique تعریف نکرده باشیم. هر چند که اگر فیلدها را نیز Unique تعریف کرده باشیم تا یک صندلی را به چند نفر نفروشیم، در آن صورت هم برنامه دچار خطای 500 خواهد شد. پس بهتر است که حتی در زمان‌هایی هم که فیلدها را Unique تعریف میکنیم، باز هم از ورود چند درخواست همزمان به اکشن رزرو صندلی جلوگیری کنیم.

راه حل

برای حل این مشکل میتوان از Lock statement استفاده کرد که این راه حل نیز یک مشکل دارد که در ادامه به آن اشاره خواهم کرد.

Lock statement به ما این امکان رو میده تا اگر بخشی از کد ما در یک Thread در حال اجرا شدن است، Thread دیگری به آن بخش از کد، دسترسی نداشته باشد و منتظر بماند تا آن Thread کارش با کد ما تموم شود و بعد Thread جدید بتونه کد مارو اجرا کنه.

نحوه استفاده از Lock statement هم بسیار ساده‌است:

public class TestClass
{
    private static readonly object _lock1 = new();

    public void Method1()
    {
        lock (_lock1)
        {
            // Body
        }
    }
}

حالا باید کدهای خودمون رو در بخش Body اضافه کنیم تا دیگر چندین Thread به صورت همزمان، کدهای ما رو اجرا نکنند.

اما یک مشکل وجود داره و آن این است که ما نمیتوانیم در Lock statement، از کلمه کلیدی await استفاده کنیم؛ در حالیکه برای ساخت User جدید باید از await استفاده کنیم:

var result = await userManager.CreateAsync(user, model.Password);

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

[ApiController]
[Route("/api/[controller]")]
public class UserController(UserManager<User> userManager) : Controller
{
    private static readonly SemaphoreSlim Semaphore = new (initialCount: 1, maxCount: 1);

    [HttpPost]
    public async Task<IActionResult> Add(UserViewModel model)
    {
        var user = new User
        {
            UserName = model.UserName,
            Email = model.Email
        };

        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Perform user creation
            var result = await userManager.CreateAsync(user, model.Password);
            if (result.Succeeded)
            {
                return Ok();
            }
            return BadRequest(result.Errors);
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

این کلاس نیز مانند lock عمل میکند، ولی توانایی‌های بیشتری را در اختیار ما قرار میدهد؛ برای مثال میتوان تعیین کرد که همزمان چند ترد میتوانند به این کد دسترسی داشته باشند؛ در حالیکه در lock statement فقط یک Thread میتوانست به کد دسترسی داشته باشد. مزیت دیگر کلاس SemaphoreSlim این است که میتوان برای اجرای کدمان Timeout در نظر گرفت تا از بلاک شدن نامحدود Thread جلوگیری کنیم.

با فراخوانی await semaphore.WaitAsync، دسترسی کد ما توسط سایر Thread ها محدود و با فراخوانی Release، کد ما توسط سایر Thread ها قابل دسترسی می‌شود.

مشکل قفل کردن Thread ها

هنگام قفل کردن Thread ها، مشکلی وجود دارد و آن این است که اگر برنامه‌ی ما روی چندین سرور مختلف اجرا شود، این روش جوابگو نخواهد بود؛ چون قفل کردن Thread روی یک سرور تاثیری در سایر سرورها جهت محدود کردن دسترسی به کد ما ندارد. اما به صورت کلی میتوان از این روش برای بخش‌هایی خاص از برنامه‌هایمان استفاده کنیم.

پیاده سازی با کمک الگوی AOP

برای اینکه کارمون راحت تر بشه، میتونیم کدهای بالا رو به یک Attribute انتقال بدیم و از اون Attribute در بالای اکشن‌هامون استفاده کنیم تا کل عملیات اکشن‌هامونو رو در یک Thread قفل کنیم:

[AttributeUsage(AttributeTargets.Method)]
public class SemaphoreLockAttribute : Attribute, IAsyncActionFilter
{
    private static readonly SemaphoreSlim Semaphore = new (1, 1);

    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // Acquire the semaphore
        await Semaphore.WaitAsync();
        try
        {
            // Proceed with the action
            await next();
        }
        finally
        {
            // Release the semaphore
            Semaphore.Release();
        }
    }
}

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

[HttpPost]
[SemaphoreLock]
public async Task<IActionResult> Add(UserViewModel model)
{
    var user = new User
    {
        UserName = model.UserName,
        Email = model.Email
    };

    var result = await userManager.CreateAsync(user, model.Password);
    if (result.Succeeded)
    {
        return Ok();
    }
    return BadRequest(result.Errors);
}
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
مرورگرهای جدید با استفاده از ویژگی webkitdirectory، امکان انتخاب یک پوشه و تمام زیر پوشه‌های آن‌را دارند:
<form method="post" asp-action="UploadFiles" asp-controller="Home" enctype="multipart/form-data">
    <input type="file" name="files" webkitdirectory />
    <input type="submit" value="Upload" />
</form>
در این حالت کدهای سمت سرور آن به صورت زیر تغییر می‌کند:
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace UploadFolderASPNETCore.Controllers
{
    public class HomeController : Controller
    {
        private readonly IWebHostEnvironment _environment;
        private const int MaxBufferSize = 0x10000;

        public HomeController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        public IActionResult Index()
        {
            return View();
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> UploadFiles(IList<IFormFile> files)
        {
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            CreateDir(uploadsRootFolder);

            foreach (var file in files)
            {
                var dirPath = Path.GetDirectoryName(file.FileName);
                CreateDir(Path.Combine(uploadsRootFolder, dirPath));

                var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                using (var fileStream = new FileStream(filePath, FileMode.Create,
                                                        FileAccess.Write,
                                                        FileShare.None,
                                                        MaxBufferSize,
                                                        useAsync: true
                                                        ))
                {
                    await file.CopyToAsync(fileStream);
                }
            }

            return RedirectToAction("Index");
        }

        private void CreateDir(string path)
        {
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
        }
    }
}
نظرات مطالب
کار با وب سرویس جاوایی تشخیص ایمیل‌های موقتی در دات نت
با سلام و تشکر
در هنگام اجرای مثل با خطای زیر مواجه می‌شوم
An unhandled exception of type 'System.ServiceModel.ProtocolException' occurred in mscorlib.dll

Additional information: The content type text/html of the response message does not match the content type of the binding (text/xml; charset=utf-8). If using a custom encoder, be sure that the IsContentTypeSupported method is implemented properly. The first 13 bytes of the response were: '<HTML></HTML>'. 
مطالب
کامپوننت‌ها در AngularJS 1.5 - قسمت دوم - مسیریابی
در این قسمت به معرفی سیستم مسیریاب در Angular 1.5 خواهیم پرداخت. قبل از معرفی این سیستم ابتدا سیستم مسیریاب اصلی در Angular را بررسی خواهیم کرد.

مروری بر مسیریابی در AngularJS
برای استفاده از مسیریاب اصلی Angular کافی است از دایرکتیو ویژه‌ایی با نام ng-view به همراه یکسری تنظیمات پیکربندی استفاده کنیم. به عنوان مثال اگر آدرس صفحه با home/ مطابقت داشته باشد، تمپلیت home.html توسط دایرکتیو ng-view بارگذاری خواهد شد. برای فعال‌سازی این سیستم ابتدا باید پکیج angular-route را به پروژه مثال قسمت قبل اضافه کنید:
bower install angular-route --save
در ادامه لازم است وابستگی فوق را به صفحه‌ی index.html اضافه نمائید:
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
اکنون درون صفحه به جای نمایش مستقیم کامپوننت، از دایرکتیو ng-view استفاده خواهیم کرد:
<div class="col-md-9">
                <ng-view></ng-view>
</div>

 همچنین مقدار فیلد url کامپوننت dntWidget را به صورت زیر تغییر دهید:
      model.panel = {
          title: "Panel Title",
          items: [
              {
                  title: "Home", url: "#/home"
              },
              {
                  title: "Articles", url: "#/articles"
              },
              {
                  title: "Authors", url: "#/authors"
              }
          ]
      };

در ادامه باید سیستم مسیریاب را به عنوان یک وابستگی به اپلیکیشن معرفی کنیم:
var module = angular.module("dntModule", ["ngRoute"]);
اکنون می‌توانیم پیکربندی موردنظر جهت هدایت آدرس‌ها به تمپلیت‌های مربوطه را بنویسیم:
module.config(function ($routeProvider) {
        $routeProvider
            .when("/home", { template: "<app-home></app-home>" })
            .when("/articles", { template: "<app-articles></app-articles>" })
            .when("/authors", { template: "<app-authors></app-authors>" })
            .otherwise({ redirectTo: "/home" });
    });

همانطور که مشاهده می‌کنید به route provider اعلام کرده‌ایم که در صورت مطابقت داشتن آدرس URL با هر کدام از حالت‌های فوق، تمپلیت متناسب با آن را نمایش بدهد. در نهایت توسط otherwise اگر آدرس، با هیچکدام از حالت‌های تعریف شده مطابقت نداشت، کاربر به آدرس home/ هدایت خواهد شد. 
نکته‌ایی که در کد فوق وجود دارد این است که سیستم مسیریاب اصلی Angular تا اینجا هیچ اطلاعی از وجود کامپوننت‌ها ندارد، اما می‌داند یک تمپلیت چیست. بنابراین از تمپلیت، جهت نمایش یک کامپوننت استفاده خواهد کرد.
برای ایجاد کامپوننت‌های فوق نیز می‌توانید آن را به صورت زیر ایجاد کنید:
module.component("appHome", {
        template: `
        <hr><div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });
    module.component("appArticles", {
        template: `
        <hr><div>
            <div>Panel heading = Articles</div>
            <div>
                Articles
            </div>
        </div>`
    });
    module.component("appAuthors", {
        template: `
        <hr><div>
            <div>Panel heading = Authors</div>
            <div>
                Authors
            </div>
        </div>`
    });

اکنون اگر برنامه را اجرا کنید، خواهید دید که به صورت پیش‌فرض به آدرس home/# هدایت خواهیم شد. زیرا آدرسی برای root، درون route configuration تعریف نکرده‌ایم:

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


معرفی Component Router

مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننت‌ها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:

bower install angular-component-router --save

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

<script src="bower_components/angular-component-router/angular_1_router.js"></script>

همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:

var module = angular.module("dntModule", ["ngComponentRouter"]);

در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:

module.component("appHome", {
        template: `
        <hr>
        <div>
            <div>Panel heading = HomePage</div>
            <div>
                HomePage
            </div>
        </div>`
    });

همانطور که مشاهده می‌کنید برای پاسخ‌گویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کرده‌ایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کرده‌ایم.

تمپلیت کامپوننت فوق نیز به صورت زیر است:

<div class="container">
    <div class="row">
        <div class="col-md-3">
            <hr>
            <dnt-widget></dnt-widget>
        </div>
        <div class="col-md-9">
            <ng-outlet></ng-outlet>
        </div>
    </div>
</div>

لازم به ذکر است دیگر نباید از دایرکتیو  ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحه‌ی index.html از کامپوننت فوق استفاده خواهیم کرد:

<html ng-app="dntModule">
<head>
    <meta charset="UTF-8">
    <title>Using Angular 1.5 Component Router</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
</head>
<body>
    
    <dnt-app></dnt-app>

    <script src="bower_components/angular/angular.js" type="text/javascript"></script>
    <script src="bower_components/angular-component-router/angular_1_router.js"></script>
    <script src="scripts/module.js" type="text/javascript"></script>
    <script src="scripts/dnt-app.component.js"></script>
    <script src="scripts/dnt-widget.component.js"></script>
</body>
</html>

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

module.value("$routerRootComponent", "dntApp");

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

کدهای این قسمت را نیز از اینجا می‌توانید دریافت کنید. 

مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
jqGrid یکی از افزونه‌های بسیار محبوب jQuery جهت نمایش جدول مانند اطلاعات، در سمت کلاینت است. توانمندی‌های آن صرفا به نمایش ستون‌ها و ردیف‌ها خلاصه نمی‌شود. قابلیت‌هایی مانند صفحه بندی، مرتب سازی، جستجو، ویرایش توکار، تولید خودکار صفحات افزودن رکوردها، اعتبارسنجی داده‌ها، گروه بندی، نمایش درختی و غیره را نیز به همراه دارد. همچنین به صورت توکار پشتیبانی از راست به چپ را نیز لحاظ کرده‌است.
 مجوز استفاده از فایل‌های جاوا اسکریپتی آن MIT است؛ به این معنا که در هر نوع پروژه‌ای قابل استفاده است. مجوز استفاده از کامپوننت‌های سمت سرور آن که برای نمونه جهت ASP.NET MVC یک سری HTML Helper را تدارک دیده‌اند، تجاری می‌باشد. در ادامه قصد داریم صرفا از فایل‌های JS عمومی آن استفاده کنیم.


دریافت jqGrid

برای دریافت jqGrid می‌توانید به مخزن کد آن، در آدرس https://github.com/tonytomov/jqGrid/releases و یا از طریق NuGet اقدام کنید:
 PM> Install-Package Trirand.jqGrid
استفاده از NuGet بیشتر توصیه می‌شود، زیرا به صورت خودکار وابستگی‌های jQuery و همچنین jQuery UI آن‌را نیز به همراه داشته و نصب خواهد کرد.
از jQuery UI برای تولید صفحات جستجوی بر روی رکوردها و همچنین تولید خودکار صفحات ویرایش و یا افزودن رکوردها استفاده می‌کند. به علاوه آیکن‌ها، قالب و رنگ خود را نیز از jQuery UI دریافت می‌کند. بنابراین اگر قصد تغییر قالب آن‌را داشتید تنها کافی است یک قالب استاندارد دیگر jQuery UI را مورد استفاده قرار دهید.


تنظیمات اولیه فایل Layout سایت

پس از دریافت بسته‌ی نیوگت jqGrid، نیاز است فایل‌های مورد نیاز اصلی آن‌را به شکل زیر به فایل layout پروژه اضافه کرد:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    
    <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" />
    <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" />
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.7.2.min.js"></script>
    <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script>
    <script src="~/Scripts/i18n/grid.locale-fa.js"></script>
    <script src="~/Scripts/jquery.jqGrid.min.js"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>
فایل jquery.ui.all.css شامل تمامی فایل‌های CSS مرتبط با jQuery UI است و نیازی نیست تا سایر فایل‌های آن‌را لحاظ کرد.
این گرید به همراه فایل زبان فارسی grid.locale-fa.js نیز می‌باشد که در کدهای فوق پیوست شده‌است. البته اگر فرصت کردید نیاز است کمی ترجمه‌های آن بهبود پیدا کنند.


تنظیمات ثانویه site.css

.ui-widget {
}

/*how to move jQuery dialog close (X) button from right to left*/
.ui-jqgrid .ui-jqgrid-caption-rtl {
    text-align: center !important;
}

.ui-dialog .ui-dialog-titlebar-close {
    left: .3em !important;
}

.ui-dialog .ui-dialog-title {
    margin: .1em 0 .1em .8em !important;
    direction: rtl !important;
    float: right !important;
}
احتمالا تنظیمات قلم‌های jQuery UI و یا jqGrid مدنظر شما نیستند و نیاز به تعویض دارند. در اینجا نحوه‌ی بازنویسی آن‌ها را ملاحظه می‌کنید.
همچنین محل قرار گیری دکمه‌ی بسته شدن دیالوگ‌ها و راست به چپ کردن عناوین آن‌ها نیز در اینجا قید شده‌اند.


مدل برنامه

در ادامه قصد داریم لیستی از محصولات را با ساختار ذیل، توسط jqGrid نمایش دهیم:
namespace jqGrid01.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}


ساختار داده‌ای مورد نیاز توسط jqGrid

jqGrid مستقل است از فناوری سمت سرور. بنابراین هر چند در عنوان بحث ASP.NET MVC ذکر شده‌است، اما از ASP.NET MVC صرفا جهت بازگرداندن خروجی JSON استفاده خواهیم کرد و این مورد در هر فناوری سمت سرور دیگری نیز می‌تواند انجام شود.
using System.Collections.Generic;

namespace jqGrid01.Models
{
    public class JqGridData
    {
        public int Total { get; set; }

        public int Page { get; set; }

        public int Records { get; set; }

        public IList<JqGridRowData> Rows { get; set; }

        public object UserData { get; set; }
    }

    public class JqGridRowData
    {
        public int Id { set; get; }
        public IList<string> RowCells { set; get; }
    }
}
خروجی JSON مدنظر توسط jqGrid، یک چنین ساختاری را باید داشته باشد.
Total، نمایانگر تعداد صفحات اطلاعات است. عدد Page، شماره صفحه‌ی جاری است. عدد Records، تعداد کل رکوردهای گزارش را مشخص می‌کند. ساختار ردیف‌های آن نیز تشکیل شده‌است از یک Id به همراه سلول‌هایی که باید با فرمت string، بازگشت داده شوند.
UserData اختیاری است. برای مثال اگر خواستید جمع کل صفحه را در ذیل گرید نمایش دهید، می‌توانید یک anonymous object را در اینجا مقدار دهی کنید. خاصیت‌های آن دقیقا باید با نام خاصیت‌های ستون‌های متناظر، یکی باشند. برای مثال اگر می‌خواهید عددی را در ستون Id، در فوتر گرید نمایش دهید، باید نام خاصیت را Id ذکر کنید.


کدهای سمت کلاینت گرید

در اینجا کدهای کامل سمت کلاینت گرید را ملاحظه می‌کنید:
@{
    ViewBag.Title = "Index";
}

<div dir="rtl" align="center">
    <div id="rsperror"></div>
    <table id="list" cellpadding="0" cellspacing="0"></table>
    <div id="pager" style="text-align:center;"></div>
</div>

@section Scripts
{
    <script type="text/javascript">
        $(document).ready(function () {
            $('#list').jqGrid({
                caption: "آزمایش اول",
                //url from wich data should be requested
                url: '@Url.Action("GetProducts","Home")',
                //type of data
                datatype: 'json',
                jsonReader: { 
                    root: "Rows",
                    page: "Page",
                    total: "Total",
                    records: "Records",
                    repeatitems: true,
                    userdata: "UserData",
                    id: "Id",
                    cell: "RowCells"
                },
                //url access method type
                mtype: 'GET',
                //columns names
                colNames: ['شماره', 'نام محصول', 'موجود است', 'قیمت'],
                //columns model
                colModel: [
                { name: 'Id', index: 'Id', align: 'right', width: 50, sorttype: "number" },
                { name: 'Name', index: 'Name', align: 'right', width: 300 },
                { name: 'IsAvailable', index: 'IsAvailable', align: 'center', width: 100, formatter: 'checkbox' },
                { name: 'Price', index: 'Price', align: 'center', width: 100, sorttype: "number" }
                ],
                //pager for grid
                pager: $('#pager'),
                //number of rows per page
                rowNum: 10,
                rowList: [10, 20, 50, 100],
                //initial sorting column
                sortname: 'Id',
                //initial sorting direction
                sortorder: 'asc',
                //we want to display total records count
                viewrecords: true,
                altRows: true,
                shrinkToFit: true,
                width: 'auto',
                height: 'auto',
                hidegrid: false,
                direction: "rtl",
                gridview: true,
                rownumbers: true,
                footerrow: true,
                userDataOnFooter: true,
                loadComplete: function() {
                    //change alternate rows color
                    $("tr.jqgrow:odd").css("background", "#E0E0E0");
                },
                loadError: function(xhr, st, err) {
                     jQuery("#rsperror").html("Type: " + st + "; Response: " + xhr.status + " " + xhr.statusText);
                }
                //, loadonce: true
            })
            .jqGrid('navGrid', "#pager",
            {
                edit: false, add: false, del: false, search: false,
                refresh: true
            })
            .jqGrid('navButtonAdd', '#pager',
            {
                caption: "تنظیم نمایش ستون‌ها", title: "Reorder Columns",
                onClickButton: function() {
                     jQuery("#list").jqGrid('columnChooser');
                }
            });
        });
    </script>
}
- برای نمایش این گرید، به یک جدول و یک div نیاز است. از جدول با id مساوی list جهت نمایش رکوردهای برنامه استفاده می‌شود. از div با id مساوی pager برای نمایش اطلاعات صفحه بندی و نوار ابزار پایین گرید کمک گرفته خواهد شد.
Div سومی با id مساوی rsperror نیز تعریف شده‌است که از آن جهت نمایش خطاهای بازگشت داده شده از سرور استفاده کرده‌ایم.
- در ادامه نحوه‌ی فراخوانی افزونه‌ی jqGrid را بر روی جدول list ملاحظه می‌کنید.
- خاصیت caption، عنوان نمایش داده شده در بالای گرید را مقدار دهی می‌کند:


- خاصیت url، به آدرسی اشاره می‌کند که قرار است ساختار JqGridData ایی را که پیشتر در مورد آن بحث کردیم، با فرمت JSON بازگشت دهد. در اینجا برای مثال به یک اکشن متد کنترلری در یک پروژه‌ی ASP.NET MVC اشاره می‌کند.
- datatype را برابر json قرار داده‌ایم. از نوع xml نیز پشتیبانی می‌کند.
- شیء jsonReader را از این جهت مقدار دهی کرده‌ایم تا بتوانیم شیء JqGridData را با اصول نامگذاری دات نت، هماهنگ کنیم. برای درک بهتر این موضوع، فایل jquery.jqGrid.src.js را باز کنید و در آن به دنبال تعریف jsonReader بگردید. به یک چنین مقادیر پیش فرضی خواهید رسید:
ts.p.jsonReader = $.extend(true,{
root: "rows",
page: "page",
total: "total",
records: "records",
repeatitems: true,
cell: "cell",
id: "id",
userdata: "userdata",
subgrid: {root:"rows", repeatitems: true, cell:"cell"}
},ts.p.jsonReader);
برای مثال سلول‌ها را با نام cell دریافت می‌کند که در شیء JqGridData به RowCells تغییر نام یافته‌است. برای اینکه این تغییر نام‌ها توسط jqGrid پردازش شوند، تنها کافی است jsonReader را مطابق تعاریفی که ملاحظه می‌کنید، مقدار دهی کرد.
- در ادامه mtype به GET تنظیم شده‌است. در اینجا مشخص می‌کنیم که عملیات Ajax ایی دریافت اطلاعات از سرور توسط GET انجام شود یا برای مثال توسط POST.
- خاصیت colNames، معرف نام ستون‌های گرید است. برای اینکه این نام‌ها از راست به چپ نمایش داده شوند، باید خاصیت direction به rtl تنظیم شود.
- colModel آرایه‌ای است که تعاریف ستون‌ها را در بر دارد. مقدار name آن باید یک نام منحصربفرد باشد. از این نام در حین جستجو یا ویرایش اطلاعات استفاده می‌شود. مقدار index نامی است که جهت مرتب سازی اطلاعات، به سرور ارسال می‌شود. تنظیم sorttype در اینجا مشخص می‌کند که آیا به صورت پیش فرض، ستون جاری رشته‌ای مرتب شود یا اینکه برای مثال عددی پردازش گردد. مقادیر مجاز آن text (مقدار پیش فرض)، float، number، currency، numeric، int ، integer، date و datetime هستند.
- در ستون IsAvailable، مقدار formatter نیز تنظیم شده‌است. در اینجا توسط formatter، نوع bool دریافتی با یک checkbox نمایش داده خواهد شد.
- خاصیت pager به id متناظری در صفحه اشاره می‌کند.
- توسط rowNum مشخص می‌کنیم که در هر صفحه چه تعداد رکورد باید نمایش داده شوند.
- تعداد رکوردهای نمایش داده شده را می‌توان توسط rowList پویا کرد. در اینجا آرایه‌ای را ملاحظه می‌کنید که توسط اعداد آن، کاربر امکان انتخاب صفحاتی مثلا 100 ردیفه را نیز پیدا می‌کند. rowList به صورت یک dropdown در کنار عناصر راهبری صفحه در فوتر گرید ظاهر می‌شود.
- خاصیت sortname، نحوه‌ی مرتب سازی اولیه گرید را مشخص می‌کند.
- خاصیت sortorder، جهت مرتب سازی اولیه‌ی گردید را تنظیم می‌کند.
- viewrecords: تعداد رکوردها را در نوار ابزار پایین گرید نمایش می‌دهد.
- altRows: سبب می‌شود رنگ متن ردیف‌ها یک در میان متفاوت باشد.
- shrinkToFit: به معنای تنظیم خودکار اندازه‌ی سلول‌ها بر اساس اندازه‌ی داده‌ای است که دریافت می‌کنند.
- width: عرض گرید، که در اینجا به auto تنظیم شده‌است.
- height: طول گرید، که در اینجا به auto جهت محاسبه‌ی خودکار، تنظیم شده‌است.
- gridview: برای بالا بردن سرعت نمایشی به true تنظیم شده‌است. در این حالت کل ردیف یکباره درج می‌شود. اگر از subgird یا حالت نمایش درختی استفاده شود، باید این خاصیت را false کرد.
- rownumbers: ستون سمت راست شماره ردیف‌های خودکار را نمایش می‌دهد.
- footerrow: سبب نمایش ردیف فوتر می‌شود.
- userDataOnFooter: سبب خواهد شد تا خاصیت UserData مقدار دهی شده، در ردیف فوتر ظاهر شود.
- loadComplete : یک callback است که زمان پایان بارگذاری صفحه‌ی جاری را مشخص می‌کند. در اینجا با استفاده از jQuery سبب شده‌ایم تا رنگ پس زمینه‌ی ردیف‌ها یک در میان تغییر کند.
- loadError: اگر از سمت سرور خطایی صادر شود، در این callback قابل دریافت خواهد بود.
- در ادامه توسط فراخوانی متد jqGrid با پارامتر navGrid، در ناحیه pager سبب نمایش دکمه refresh شده‌ایم. این دکمه سبب بارگذاری مجدد اطلاعات گردید از سرور می‌شود.
- همچنین به کمک متد jqGrid با پارامتر navButtonAdd در ناحیه pager، سبب نمایش دکمه‌ای که صفحه‌ی انتخاب ستون‌ها را ظاهر می‌کند، خواهیم شد.



پیشنیاز کدهای سمت سرور jqGrid

اگر به تنظیمات گرید دقت کرده باشید، خاصیت index ستون‌ها، نامی است که به سرور، جهت اطلاع رسانی در مورد فیلتر اطلاعات و مرتب سازی مجدد آن‌ها ارسال می‌گردد. این نام، بر اساس کلیک کاربر بر روی ستون‌های موجود، هر بار می‌توان متفاوت باشد. بنابراین بجای if و else نوشتن‌های طولانی جهت مرتب سازی اطلاعات، می‌توان از کتابخانه‌ی معروفی به نام dynamic LINQ استفاده کرد.
 PM> Install-Package DynamicQuery
به این ترتیب می‌توان قسمت orderby را به صورت پویا و با رشته‌ای دریافتی، مقدار دهی کرد.


کدهای سمت سرور بازگشت اطلاعات به فرمت JSON

در کدهای سمت کلاینت، به اکشن متد GetProducts اشاره شده بود. تعاریف کامل آن‌را در ذیل مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using jqGrid01.Models;
using jqGrid01.Extensions; // for dynamic OrderBy

namespace jqGrid01.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult GetProducts(string sidx, string sord, int page, int rows)
        {
            var list = ProductDataSource.LatestProducts;

            var pageIndex = page - 1;
            var pageSize = rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var products = list.AsQueryable()
                               .OrderBy(sidx + " " + sord)
                               .Skip(pageIndex * pageSize)
                               .Take(pageSize)
                               .ToList();

            var jqGridData = new JqGridData
            {
                UserData = new // نمایش در فوتر
                {
                    Name = "جمع صفحه",
                    Price = products.Sum(x => x.Price)
                },
                Total = totalPages,
                Page = page,
                Records = totalRecords,
                Rows = (products.Select(product => new JqGridRowData
                                                {
                                                    Id = product.Id,
                                                    RowCells = new List<string>
                                                    {
                                                        product.Id.ToString(CultureInfo.InvariantCulture),
                                                        product.Name,
                                                        product.IsAvailable.ToString(),
                                                        product.Price.ToString(CultureInfo.InvariantCulture)
                                                    }
                                                })).ToList()
            };
            return Json(jqGridData, JsonRequestBehavior.AllowGet);
        }
    }
}
- سطر ProductDataSource.LatestProducts چیزی نیست بجز لیست جنریکی از محصولات.
- امضای متد GetProducts نیز مهم است. دقیقا همین پارامترها با همین نام‌ها از طرف jqGrid به سرور ارسال می‌شوند که توسط آن‌ها ستون مرتب سازی، جهت مرتب سازی، صفحه‌ی جاری و تعداد ردیفی که باید بازگشت داده شوند، قابل دریافت است.
- در این کدها دو قسمت مهم وجود دارند:
الف) متد OrderBy نوشته شده، به صورت پویا عمل می‌کند و از کتابخانه‌ی Dynamic LINQ مایکروسافت بهره می‌برد.
به علاوه توسط Take و Skip کار صفحه بندی و بازگشت تنها بازه‌ای از اطلاعات مورد نیاز، انجام می‌شود.
ب) لیست جنریک محصولات، در نهایت باید با فرمت JqGridData به صورت JSON بازگشت داده شود. نحوه‌ی این Projection را در اینجا می‌توانید ملاحظه کنید.
هر ردیف این لیست، باید تبدیل شود به ردیفی از جنس JqGridRowData، تا توسط jqGrid قابل پردازش گردد.
- توسط مقدار دهی UserData، برچسبی را در ذیل ستون Name و مقداری را در ذیل ستون Price نمایش خواهیم داد.


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

بهترین راهنمای جزئیات این Grid، مستندات آنلاین آن هستند: http://www.trirand.com/jqgridwiki/doku.php?id=wiki:jqgriddocs
همچنین این مستندات را با فرمت PDF نیز می‌توانید مطالعه کنید: http://www.trirand.com/blog/jqgrid/downloads/jqgriddocs.pdf


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

مثال‌های سری jqGrid تغییرات زیادی داشتند. برای دریافت آن‌ها به این مخزن کد مراجعه کنید. 
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 15 - بررسی تغییرات Caching
سلام؛ من در صفحه جزئیات محصول، یک فایل css دارم که اونو از دیتابیس پر می‌کنم و برای یک محصول بصورت جدا از دیتابیس بصورت زیر پر میشود:
@section content{

    <link href=@Url.RouteUrl(new { controller="Home", action="GetCss" ,styles=Model.CssBody}) rel="stylesheet" type="text/css" />
}
زمانیکه از کش استاتیک استفاده کنم و بخواهم زمان رو مثلا 6 ماه کش در نظر بگیرم، آیا این فایل css را که در صفحه جزییات هم پر میشود، در کش در نظر می‌گیرد؟
مطالب
مراحل ارسال یک پروژه‌ی Visual Studio به GitHub
از نگارش 2012 ویژوال استودیو، امکان کار با مخازن Git، به صورت یکپارچه و توکار و بدون نیاز به ابزارهای جانبی، توسط آن فراهم شده‌است. در ادامه قصد داریم به کمک این ویژگی توکار، نحوه‌ی ارسال یک پروژه‌ی از پیش موجود VS.NET را برای اولین بار به GitHub بررسی کنیم.


تنظیمات مقدماتی GitHub

در ابتدا نیاز است یک مخزن کد خالی را در GitHub ایجاد کنید. برای این منظور به برگه‌ی Repositories در اکانت GitHub خود مراجعه کرده و بر روی دکمه‌ی New کلیک کنید:


سپس در صفحه‌ی بعدی، نام پروژه را به همراه توضیحاتی وارد نمائید و بر روی دکمه‌ی Create repository کلیک کنید. در اینجا سایر گزینه‌ها را انتخاب نکنید. نیازی به انتخاب گزینه‌ی READ ME و یا انتخاب مجوز و غیره نیست. تمام این کارها را در سمت پروژه‌ی اصلی می‌توان انجام داد و یا VS.NET فایل‌های ignore را به صورت خودکار ایجاد می‌کند. در اینجا صرفا هدف، ایجاد یک مخزن کد خالی است.


از اطلاعات صفحه‌ی بعدی، تنها به آدرس مخصوص GitHub آن نیاز داریم. از این آدرس در VS.NET برای ارسال اطلاعات به سرور استفاده خواهیم کرد:



تنظیمات VS.NET برای ارسال پروژه به مخزن GitHub

پس از ایجاد یک مخزن کد خالی در GitHub، اکنون می‌توانیم پروژه‌ی خود را به آن ارسال کنیم. برای این منظور از منوی File، گزینه‌ی Add to source control را انتخاب کنید و در صفحه‌ی باز شده، گزینه‌ی Git را انتخاب نمائید:



سپس در کنار برگه‌ی Solution Explorer، برگه‌ی Team Explorer را انتخاب کنید. در اینجا بر روی دکمه‌ی Home در نوار ابزار آن کلیک کرده و سپس بر روی دکمه‌ی Unsynced commits کلیک نمائید.


در ادامه در صفحه‌ی باز شده، همان آدرس مخصوص مخزن کد جدید را در GitHub وارد کرده و بر روی دکمه‌ی Publish کلیک کنید:


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


به این ترتیب عملیات Publish اولیه انجام شده و تصویر ذیل نمایان خواهد شد:


در اینجا بر روی دکمه‌ی Sync کلیک کنید. به این ترتیب مخزن کد GitHub به پروژه‌ی جاری متصل خواهد شد:


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


در اینجا پیام اولین ارسال را وارد کرده و سپس بر روی دکمه‌ی Commit کلیک کنید:


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



اندکی صبر کنید تا فایل‌ها به سرور ارسال شوند. اکنون اگر به GitHub مراجعه کنید، فایل‌های ارسالی قابل مشاهده هستند:



اعمال تغییرات بر روی پروژه‌ی محلی و ارسال به سرور

در ادامه می‌خواهیم دو فایل README.md و LICENSE.md را به پروژه اضافه کنیم. پس از افزودن آن‌ها، یا هر تغییر دیگری در پروژه، اینبار برای ارسال تغییرات به سرور، تنها کافی است به برگه‌ی Team explorer مراجعه کرده و ابتدا بر روی دکمه‌ی Home کلیک کرد تا منوی انتخاب گزینه‌‌های آن ظاهر شود. در اینجا تنها کافی است گزینه‌ی Changes را انتخاب و دقیقا همان مراحل عنوان شده‌ی پیشین را تکرار کرد. ابتدا ورود پیام Commit و سپس Commit. در ادامه Sync محلی و سپس Sync با سرور.
مطالب
مسیریابی در 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 برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
مطالب
Globalization در ASP.NET MVC - قسمت چهارم
در قسمت قبل مقدمه ای راجع به انواع منابع موجود در ASP.NET و برخی مسائل پیرامون آن ارائه شد. در این قسمت راجع به نحوه رفتار ASP.NET در برخورد با انواع منابع بحث می‌شود.

مدیریت منابع در ASP.NET 
در مدل پرووایدر منابع در ASP.NET کار مدیریت منابع از کلاس ResourceProviderFactory شروع می‌شود. این کلاس که از نوع abstract تعریف شده است، دو متد برای فراهم کردن پرووایدرهای کلی و محلی دارد.
کلاس پیش‌فرض در ASP.NET برای پیاده‌سازی ResourceProviderFactory در اسمبلی System.Web قرار دارد. این کلاس که ResXResourceProviderFactory نام دارد نمونه‌هایی از کلاس‌های LocalResxResourceProvider و GlobalResxResourceProvider را برمی‌گرداند. درباره این کلاس‌ها در ادامه بیشتر بحث خواهد شد.

نکته: هر سه کلاس پیش‌فرض اشاره شده در بالا و نیز سایر کلاس‌های مربوط به عملیات مدیریت منابع در آن‌ها، همگی در فضای نام System.Web.Compilation قرار دارند و متاسفانه دارای سطح دسترسی internal هستند. بنابراین به صورت مستقیم در دسترس نیستند.

برای نمونه با توجه به تصویر فرضی نشان داده شده در قسمت قبل، در اولین بارگذاری صفحه SubDir1\Page1.aspx عبارات ضمنی بکاربرده شده در این صفحه برای منابع محلی (در قسمت قبل شرح داده شده است) باعث فراخوانی متد مربوط به Local Resources در کلاس ResXResourceProviderFactory می‌شود. این متد نمونه‌ای از کلاس LocalResXResourceProvider برمی‌گرداند. (در ادامه با نحوه سفارشی‌سازی این کلاس‌ها نیز آشنا خواهیم شد).
رفتار پیش‌فرض این پرووایدر این است که نمونه‌ای از کلاس ResourceManager با توجه به کلید درخواستی برای صفحه موردنظر (مثلا نوع Page1.aspx در اسمبلی App_LocalResources.subdir1.XXXXXX که در تصویر موجود در قسمت قبل نشان داده شده است) تولید می‌کند. حال این کلاس با استفاده از کالچر مربوط به درخواست موردنظر، ورودی موردنظر را از منبع مربوطه استخراج می‌کند. مثلا اگر کالچر موردبحث es (اسپانیایی) باشد، اسمبلی ستلایت موجود در مسیر نسبی \es\ انتخاب می‌شود.
برای روشن‌تر شدن بحث به تصویر زیر که عملیات مدیریت منابع پیش فرض در ASP.NET در درخواست صفحه Page1.aspx از پوشه SubDir1 را نشان می‌دهد، دقت کنید:

همانطور که در قسمت اول این سری مطالب عنوان شد، رفتار کلاس ResourceManager برای یافتن کلیدهای Resource، استخراج آن از نزدیکترین گزینه موجود است. یعنی مثلا برای یافتن کلیدی در کالچر es در مثال بالا، ابتدا اسمبلی‌های مربوط به این کالچر جستجو می‌شود و اگر ورودی موردنظر یافته نشد، جستجو در اسمبلی‌های ستلایت پیش‌فرض سیستم موجود در ریشه فولدر bin برنامه ادامه می‌یابد، تا درنهایت نزدیک‌ترین گزینه پیدا شود (فرایند fallback).

نکته: همانطور که در تصویر بالا نیز مشخص است، نحوه نامگذاری اسمبلی منابع محلی به صورت <App_LocalResources.<SubDirectory>.<A random code است.

نکته: پس از اولین بارگذاری هر اسمبلی، آن اسمبلی به همراه خود نمونه کلاس ResourceManager که مثلا توسط کلاس LocalResXResourceProvider تولید شده است در حافظه سرور کش می‌شوند تا در استفاده‌های بعدی به کار روند.

نکته: فرایند مشابه‌ای برای یافتن کلیدها در منابع کلی (Global Resources) به انجام می‌رسد. تنها تفاوت آن این است که کلاس ResXResourceProviderFactory نمونه‌ای از کلاس GlobalResXResourceProvider تولید می‌کند.

چرا پرووایدر سفارشی؟
تا اینجا بالا با کلیات عملیاتی که ASP.NET برای بارگذاری منابع محلی و کلی به انجام می‌رساند، آشنا شدیم. حالا باید به این پرسش پاسخ داد که چرا پرووایدری سفارشی نیاز است؟ علاوه بر دلایلی که در قسمت‌های قبلی به آنها اشاره شد، می‌توان دلایل زیر را نیز برشمرد:
 
- استفاده از منابع و یا اسمبلی‌های ستلایت موجود - اگر بخواهید در برنامه خود از اسمبلی‌هایی مشترک، بین برنامه‌های ویندوزی و وبی استفاده کنید، و یا بخواهید به هردلیلی از اسمبلی‌های جداگانه‌ای برای این منابع استفاده کنید، مدل پیش‌فرض موجود در ASP.NET جوابگو نخواهد بود.

- استفاده از منابع دیگری به غیر از فایلهای resx. مثل دیتابیس - برای برنامه‌های تحت وب که صفحات بسیار زیاد به همراه ورودی‌های بیشماری از Resourceها دارند، استفاده از مدل پرووایدر منابع پیش‌فرض در ASP.NET و ذخیره تمامی این ورودی‌ها درون فایل‌های resx. بار نسبتا زیادی روی حافظه سرور خواهد گذاشت. درصورت مدیریت بهینه فراخوانی‌های سمت دیتابیس می‌توان با بهره‌برداری از جداول یک دیتابیس به عنوان منبع، کمک زیادی به وب سرور کرد! هم‌چنین با استفاده از دیتابیس می‌توان مدیریت بهتری بر ورودی‌ها داشت و نیز امکان ذخیره‌سازی حجم بیشتری از داده‌ها در اختیار توسعه دهنده قرار خواهد گرفت.
البته به غیر از دیتابیس و فایل‌های resx. نیز گزینه‌های دیگری برای ذخیره‌سازی ورودی‌های این منابع وجود دارند. به عنوان مثال می‌توان مدیریت این منابع را کلا به سیستم دیگری سپرد و درخواست ورودی‌های موردنیاز را به یکسری وب‌سرویس سپرد. برای پیاده سازی چنین سیستمی نیاز است تا مدلی سفارشی تهیه و استفاده شود.

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

مدل پرووایدر منابع
همانطور که قبلا هم اشاره شد، وظیفه استخراج داده‌ها از Resourceها به صورت پیش‌فرض، درنهایت بر عهده نمونه‌ای از کلاس ResourceManager است. در واقع این کلاس کل فرایند انتخاب مناسب‌ترین کلید از منابع موجود را با توجه به کالچر رابط کاربری (UI Culture) در ثرد جاری کپسوله می‌کند. درباره این کلاس در ادامه بیشتر بحث خواهد شد.
هم‌چنین بازهم همانطور که قبلا توضیح داده شد، استفاده از ورودی‌های منابع موجود به دو روش انجام می‌شود. استفاده از عبارات بومی‌سازی و نیز با استفاده از برنامه‌نویسی که ازطریق دومتد GetLocalResourceObject و GetGlobalResourceObject انجام می‌شود. درضمن کلیه عبارات بومی‌سازی در زمان رندر صفحات وب درنهایت تبدیل به فراخوانی‌هایی از این دو متد در کلاس TemplateControl خواهند شد.
عملیات پس از فراخوانی این دو متد جایی است که مدل Resource Provider پیش‌فرض ASP.NET وارد کار می‌شود. این فرایند ابتدا با فراخوانی نمونه‌ای از کلاس ResourceProviderFactory آغاز می‌شود که پیاده‌سازی پیش‌فرض آن در کلاس ResXResourceProviderFactory قرار دارد.
این کلاس سپس با توجه به نوع منبع درخواستی (Global یا Local) نمونه‌ای از پرووایدر مربوطه (که باید اینترفیس IResourceProvider را پیاده‌سازی کرده باشند) را تولید می‌کند. پیاده‌سازی پیش‌فرض این پرووایدرها در ASP.NET در کلاس‌های GlobalResXResourceProvider و LocalResXResourceProvider قرار دارد.
این پروایدرها درنهایت باتوجه به محل ورودی درخواستی، نمونه مناسب از کلاس RsourceManager را تولید و استفاده می‌کنند.
هم‌چنین در پروایدرهای محلی، برای استفاده از عبارات بومی‌سازی ضمنی، نمونه‌ای از کلاس ResourceReader مورد استفاده قرار می‌گیرد. در زمان تجزیه و تحلیل صفحه وب درخواستی در سرور، با استفاده از این کلاس کلیدهای موردنظر یافته می‌شوند. این کلاس درواقع پیاده‌سازی اینترفیس IResourceReader بوده که حاوی یک Enumerator که جفت داده‌های Key-Value از کلیدهای Resource را برمی‌گرداند، است.
تصویر زیر نمایی کلی از فرایند پیش‌فرض موردبحث را نشان می‌دهد:

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

پیاده‌سازی‌ها
کلاس ResourceProviderFactory به صورت زیر تعریف شده است:
public abstract class ResourceProviderFactory
{
    public abstract IResourceProvider CreateGlobalResourceProvider(string classKey);
    public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath);
}
همانطور که مشاهده می‌کنید دو متد برای تولید پرووایدرهای مخصوص منابع کلی و محلی در این کلاس وجود دارد. پرووایدر کلی تنها نیاز به نام کلید Resource برای یافتن داده موردنظر دارد. اما پرووایدر محلی به مسیر صفحه درخواستی برای اینکار نیاز دارد که با توجه به توضیحات ابتدای این مطلب کاملا بدیهی است.
پس از تولید پرووایدر موردنظر با استفاده از متد مناسب با توجه به شرایط شرح داده شده در بالا، نمونه تولیدشده از کلاس پرووایدر موردنظر وظیفه فراهم‌کردن کلیدهای Resource را برعهده دارد. پرووایدرهای موردبحث باید اینترفیس IResourceProvider را که به صورت زیر تعریف شده است، پیاده سازی کنند:
public interface IResourceProvider
{
    IResourceReader ResourceReader { get; }
    object GetObject(string resourceKey, CultureInfo culture);
}
همانطور که می‌بینید این پرووایدرها باید یک RsourceReader برای خواندن کلیدهای Resource فراهم کنند. همچنین یک متد با عنوان GetObject که کار اصلی برگرداندن داده ذخیره‌شده در ورودی موردنظر را برعهده دارد باید در این پرووایدرها پیاده‌سازی شود. همانطور که قبلا اشاره شد، پیاده‌سازی پیش‌فرض این کلاس‌ها درنهایت نمونه‌ای از کلاس ResourceManager را برای یافتن مناسب‌ترین گزینه از بین کلیدهای موجود تولید می‌کند. این نمونه مورد بحث در متد GetObject مورد استفاده قرار می‌گیرد. 

نکته: کدهای نشان‌داده‌شده در ادامه مطلب با استفاده از ابزار محبوب ReSharper استخراج شده‌اند. این ابزار برای دریافت این کدها معمولا از APIهای سایت SymbolSource.org استفاده می‌کند. البته منبع اصلی تمام کدهای دات نت فریمورک همان referencesource.microsoft.com است.
 
کلاس ResXResourceProviderFactory
پیاده‌سازی پیش‌فرض کلاس ResourceProviderFactory در ASP.NET که در کلاس ResXResourceProviderFactory قرار دارد، به صورت زیر است:
// Type: System.Web.Compilation.ResXResourceProviderFactory
// Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
// Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll

using System.Runtime;
using System.Web;
namespace System.Web.Compilation
{
  internal class ResXResourceProviderFactory : ResourceProviderFactory
  {
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    public ResXResourceProviderFactory()    {    }
    public override IResourceProvider CreateGlobalResourceProvider(string classKey)
    {
      return (IResourceProvider) new GlobalResXResourceProvider(classKey);
    }
    public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
    {
      return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath));
    }
  }
}
در این کلاس برای تولید پرووایدر منابع محلی از کلاس VirtualPath استفاده شده است که امکاناتی جهت استخراج مسیرهای موردنظر با توجه به مسیر نسبی و مجازی ارائه‌شده فراهم می‌کند. متاسفانه این کلاس نیز با سطح دسترسی internal تعریف شده است و امکان استفاده مستقیم از آن وجود ندارد.
 
کلاس GlobalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع کلی که در کلاس GlobalResXResourceProvider قرار دارد، به صورت زیر است:
internal class GlobalResXResourceProvider : BaseResXResourceProvider
{
  private string _classKey;
  internal GlobalResXResourceProvider(string classKey)
  {
    _classKey = classKey;
  }
  protected override ResourceManager CreateResourceManager()
  {
    string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey;
    // If there is no app resource assembly, return null
    if (BuildManager.AppResourcesAssembly == null)
      return null;
    ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly);
    resourceManager.IgnoreCase = true;
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // App resources don't support implicit resources, so the IResourceReader should never be needed 
      throw new NotSupportedException();
    }
  }
}
در این کلاس عملیات تولید نمونه مناسب از کلاس ResourceManager انجام می‌شود. مقدار BaseResourcesBuildProvider.DefaultResourcesNamespace به صورت زیر تعریف شده است:
internal const string DefaultResourcesNamespace = "Resources";
که قبلا هم درباره این مقدار پیش فرض اشاره‌ای شده بود.
پارامتر classKey درواقع اشاره به نام فایل اصلی منبع کلی دارد. مثلا اگر این مقدار برابر Resource1 باشد، کلاس ResourceManager برای نوع داده Resources.Resource1 تولید خواهد شد.
هم‌چنین اسمبلی موردنظر برای یافتن ورودی‌های منابع کلی که از BuildManager.AppResourcesAssembly دریافت شده است، به صورت پیش فرض هم‌نام با مسیر منابع کلی و با عنوان App_GlobalResources تولید می‌شود.
کلاس BuildManager فرایندهای کامپایل کدها و  صفحات برای تولید اسمبلی‌ها و نگهداری از آن‌ها در حافظه را مدیریت می‌کند. این کلاس که محتوای نسبتا مفصلی دارد (نزدیک به 2000 خط کد) به صورت public و sealed تعریف شده است. بنابراین با ریفرنس دادن اسمبلی System.Web در فضای نام System.Web.Compilation در دسترس است، اما نمی‌توان کلاسی از آن مشتق کرد. BuildManager حاوی تعداد زیادی اعضای استاتیک برای دسترسی به اطلاعات اسمبلی‌هاست. اما متاسفانه بیشتر آن‌ها سطح دسترسی عمومی ندارند.

نکته: همانطور که در بالا نیز اشاره شد، ازآنجاکه کلاس ResourceReader در اینجا تنها برای عبارات بومی سازی ضمنی کاربرد دارد، و نیز عبارات بومی‌سازی ضمنی تنها برای منابع محلی کاربرد دارند، در این کلاس برای خاصیت مربوطه در پیاده سازی اینترفیس IResourceProvider یک خطای عدم پشتیبانی (NotSupportedException) صادر شده است.

کلاس LocalResXResourceProvider
پیاده‌سازی پیش‌فرض اینترفیس IResourceProvider در ASP.NET برای منابع محلی که در کلاس LocalResXResourceProvider قرار دارد، به صورت زیر است:
internal class LocalResXResourceProvider : BaseResXResourceProvider
{
  private VirtualPath _virtualPath;
  internal LocalResXResourceProvider(VirtualPath virtualPath)
  {
    _virtualPath = virtualPath;
  }
  protected override ResourceManager CreateResourceManager()
  {
    ResourceManager resourceManager = null;
    Assembly pageResAssembly = GetLocalResourceAssembly();
    if (pageResAssembly != null)
    {
      string fileName = _virtualPath.FileName;
      resourceManager = new ResourceManager(fileName, pageResAssembly);
      resourceManager.IgnoreCase = true;
    }
    else
    {
      throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound));
    }
    return resourceManager;
  }
  public override IResourceReader ResourceReader
  {
    get
    {
      // Get the local resource assembly for this page 
      Assembly pageResAssembly = GetLocalResourceAssembly();
      if (pageResAssembly == null) return null;
      // Get the name of the embedded .resource file for this page 
      string resourceFileName = _virtualPath.FileName + ".resources";
      // Make it lower case, since GetManifestResourceStream is case sensitive 
      resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture);
      // Get the resource stream from the resource assembly
      Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName);
      // If this page has no resources, return null 
      if (resourceStream == null) return null;
      return new ResourceReader(resourceStream);
    }
  }
  [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
  private Assembly GetLocalResourceAssembly()
  {
    // Remove the page file name to get its directory
    VirtualPath virtualDir = _virtualPath.Parent;
    // Get the name of the local resource assembly
    string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir);
    BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey);
    if (result != null)
    {
      return ((BuildResultCompiledAssembly)result).ResultAssembly;
    }
    return null;
  }
}
عملیات موجود در این کلاس باتوجه به فرایندهای مربوط به یافتن اسمبلی مربوطه با استفاده از مسیر ارائه‌شده، کمی پیچیده‌تر از کلاس قبلی است.
در متد GetLocalResourceAssembly عملیات یافتن اسمبلی متناظر با درخواست جاری انجام می‌شود. اینکار باتوجه به نحوه نامگذاری اسمبلی منابع محلی که در ابتدای این مطلب اشاره شد انجام می‌شود. مثلا اگر صفحه درخواستی در مسیر SubDir1/Page1.aspx/~ باشد، در این متد با استفاده از ابزارهای موجود عنوان اسمبلی نهایی برای این مسیر که به صورت App_LocalResources.SubDir1.XXXXX است تولید و درنهایت اسمبلی مربوطه استخراج می‌شود.
درضمن در اینجا هم کلاس ResourceManager برای نوع داده متناظر با نام فایل اصلی منبع محلی تولید می‌شود. مثلا برای مسیر مجازی SubDir1/Page1.aspx/~ نوع داده‌ای با نام Page1.aspx درنظر گرفته خواهد شد (با توجه به نام فایل منبع محلی که باید به صورت Page1.aspx.resx باشد. در قسمت قبل در این باره شرح داده شده است).

نکته: کلاس SR (مخفف String Resources) که در فضای نام System.Web قرار دارد، حاوی عناوین کلیدهای Resourceهای مورداستفاده در اسمبلی System.Web است. این کلاس با سطح دسترسی internal و به صورت sealed تعریف شده است. عنوان تمامی کلیدها به صورت ثوابتی از نوع رشته تعریف شده‌‌اند.
SR درواقع یک Wrapper بر روی کلاس ResourceManager است تا از تکرار عناوین کلیدهای منابع که از نوع رشته هستند، در جاهای مختلف برنامه جلوگیری شود. کار این کلاس مشابه کاری است که کتابخانه T4MVC برای نگهداری عناوین کنترلرها و اکشن‌ها به صورت رشته‌های ثابت انجام می‌دهد. از این روش در جای جای دات نت فریمورک برای نگهداری رشته‌های ثابت استفاده شده است!
 
نکته: باتوجه به استفاده از عبارات بومی‌سازی ضمنی در استفاده از ورودی‌های منابع محلی، خاصیت ResourceReader در این کلاس نمونه‌ای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید می‌کند.

کلاس پایه BaseResXResourceProvider
کلاس پایه BaseResXResourceProvider که در دو پیاده‌سازی نشان داده شده در بالا استفاده شده است (هر دو کلاس از این کلاس مشتق شده‌اند)، به صورت زیر است: 
internal abstract class BaseResXResourceProvider : IResourceProvider
{
  private ResourceManager _resourceManager;
  ///// IResourceProvider implementation
  public virtual object GetObject(string resourceKey, CultureInfo culture)
  {
    // Attempt to get the resource manager
    EnsureResourceManager();
    // If we couldn't get a resource manager, return null 
    if (_resourceManager == null) return null;
    if (culture == null) culture = CultureInfo.CurrentUICulture;
    return _resourceManager.GetObject(resourceKey, culture);
  }
  public virtual IResourceReader ResourceReader { get { return null; } }
  ///// End of IResourceProvider implementation 
  protected abstract ResourceManager CreateResourceManager();
  private void EnsureResourceManager()
  {
    if (_resourceManager != null)  return;
    _resourceManager = CreateResourceManager();
  }
}
در این کلاس پیاده‌سازی اصلی اینترفیس IResourceProvider انجام شده است. همانطور که می‌بینید کار نهایی استخراج ورودی‌های منابع در متد GetObject با استفاده از نمونه فراهم شده از کلاس ResourceManager انجام می‌شود.

نکته: دقت کنید که در کد بالا درصورت فراهم نکردن مقداری برای کالچر، از کالچر UI در ثرد جاری (CultureInfo.CurrentUICulture) به عنوان مقدار پیش‌فرض استفاده می‌شود.

کلاس ResourceManager
در زمان اجرا ASP.NET کلید مربوط به منبع موردنظر را با استفاده از کالچر جاری UI انتخاب می‌کند. در قسمت اول این سری مطالب شرح کوتاهی بابت انواع کالچرها داده شد، اما برای توضیحات کاملتر به اینجا مراجعه کنید.
در ASP.NET به صورت پیش‌فرض تمام منابع در زمان اجرا از طریق نمونه‌ای از کلاس ResourceManager در دسترس خواهند بود. به ازای هر نوع Resource که درخواستی برای یک کلید آن ارسال می‌شود یک نمونه از کلاس ResourceManager ساخته می‌شود. در این هنگام (یعنی پس از اولین درخواست به کلیدهای یک منبع) اسمبلی ستلایت مناسب آن پس از یافته شدن (یا تولیدشدن در زمان اجرا) به دامین ASP.NET جاری بارگذاری می‌شود و تا زمانیکه این دامین Unload نشود در حافظه سرور باقی خواهد ماند.

نکته: کلاس ResourceManager تنها توانایی استخراج کلیدهای Resource از اسمبلی‌های ستلایتی (فایل‌های resources. که در قسمت اول به آن‌ها اشاره شد) که در AppDomain جاری بارگذاری شده‌اند را دارد.

کلاس ResourceManager به صورت زیر نمونه سازی می‌شود:
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
پارامتر baseName به نام کامل ریشه اسمبلی اصلی موردنظر(با فضای نام و ...) اما بدون پسوند اسمبلی مربوطه (resources.) اشاره دارد. این نام که برابر نام کلاس نهایی تولیدشده برای منبع موردنظر است همنام با فایل اصلی و پیش‌فرض منبع (فایلی که حاوی عنوان هیچ زبان و کالچری نیست) تولید می‌شود. مثلا برای اسمبلی ستلایت با عنوان MyApplication.MyResource.fa-IR.resources باید از عبارت MyApplication.MyResource استفاده شود.
پارامتر assemblyName نیز به اسمبلی حاوی اسمبلی ستلایت اصلی اشاره دارد. درواقع همان اسمبلی اصلی که نوع داده مربوط به فایل منبع اصلی درون آن embed شده است.
مثلا:
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
یا
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
 
روش دیگری نیز برای تولید نمونه‌ای از این کلاس وجود دارد که با استفاده از متد استاتیک زیر که در خود کلاس ResourceManager تعریف شده است انجام می‌شود:
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
در این متد کار استخراج ورودی‌های منابع مستقیما از فایل‌های resources. انجام می‌شود. در اینجا baseName نام فایل اصلی منبع بدون پیشوند resources. است. resourceDir نیز مسیری است که فایل‌های resources. در آن قرار دارند. usingResourceSet نیز نوع کلاس سفارشی سازی شده از ResourceSet برای استفاده به جای کلاس پیش‌فرض است که معمولا مقدار null برای آن وارد می‌شود تا از همان کلاس پیش‌فرض استفاده شود (چون برای بیشتر نیازها همین کلاس پیش‌فرض کفایت می‌کند).
 
نکته: برای تولید فایل resources. از یک فایل resx. میتوان از ابزار resgen همانند زیر استفاده کرد:
resgen d:\MyResources\MyResource.fa.resx

نکته: عملیاتی که درون کلاس ResourceManager انجام می‌شود پیچیده‌تر از آن است که به نظر می‌آید. این عملیات شامل فرایندهای بسیاری شامل بارگذاری کلیدهای مختلف یافته شده و مدیریت ذخیره موقت آن‌ها در حافظه (کش)، کنترل و مدیریت انواع Resource Setها، و مهمتر از همه مدیریت عملیات Fallback و ... که در نهایت شامل هزاران خط کد است که با یک جستجوی ساده قابل مشاهده و بررسی است (^).

نمونه‌سازی مناسب از ResourceManager
در کدهای نشان داده شده در بالا برای پیاده‌سازی پیش‌فرض در ASP.NET، مهمترین نکته همان تولید نمونه مناسب از کلاس ResourceManager است. پس از آماده شدن این کلاس عملیات استخراج ورودی‌های منابع براحتی و با مدیریت کامل انجام می‌شود. اما ازآنجاکه تقریبا تمامی APIهای موردنیاز با سطح دسترسی internal تعریف شده‌اند، متاسفانه تهیه و تولید این نمونه مناسب خارج از اسمبلی System.Web به صورت مستقیم وجود ندارد.
درهرصورت، برای آشنایی بیشتر با فرایند نشان داده شده، تولید این نمونه مناسب و استفاده مستقیم از آن می‌تواند مفید و نیز جالب باشد. پس از کمی تحقیق و با استفاده از Reflection به کدهای زیر رسیدم:
private ResourceManager CreateGlobalResourceManager(string classKey)
{
  var baseName = "Resources." + classKey;
  var buildManagerType = typeof(BuildManager);
  var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField);
  var appResourcesAssembly = (Assembly)property.GetValue(null, null);
  return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true };
}
تنها نکته کد فوق دسترسی به اسمبلی منابع کلی در خاصیت AppResourcesAssembly از کلاس BuildManager با استفاده از BindingFlagهای نشان داده شده است.
نحوه استفاده از این متد هم به صورت زیر است:
var manager = CreateGlobalResourceManager("Resource1");
Label1.Text = manager.GetString("String1");
اما برای منابع محلی کار کمی پیچیده‌تر است. کد مربوط به تولید نمونه مناسب از ResourceManager برای منابع محلی به صورت زیر خواهد بود:
private ResourceManager CreateLocalResourceManager(string virtualPath)
{
  var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true);
  var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture);
  var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true);
  var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance);
  var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static);
  var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null);

  var fileNameProperty = virtualPathType.GetProperty("FileName");
  var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null);

  var parentProperty = virtualPathType.GetProperty("Parent");
  var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null);
      
  var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent });
  var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName });
  Assembly localResourceAssembly = null;
  if (buildResultFromCache != null)
    localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null);

  if (localResourceAssembly == null)
    throw new InvalidOperationException("Unable to find the matching resource file.");

  return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true };
}
ازجمله نکات مهم این متد تولید یک نمونه از کلاس VirtualPath برای Parse کردن مسیر مجازی واردشده برای صفحه درخواستی است. از این کلاس برای بدست آوردن نام فایل منبع محلی به همراه مسیر فولدر مربوطه جهت استخراج اسمبلی متناظر استفاده میشود.
نکته مهم دیگر این کد دسترسی به متد GetLocalResourcesAssemblyName از کلاس BuildManager است که با استفاده از مسیر فولدر مربوط به صفحه درخواستی نام اسمبلی منبع محلی مربوطه را برمی‌گرداند.
درنهایت با استفاده از متد GetBuildResultFromCache از کلاس BuildManager اسمبلی موردنظر بدست می‌آید. همانطور که از نام این متد برمی‌آید این اسمبلی از کش خوانده می‌شود. البته مدیریت این اسمبلی‌ها کاملا توسط BuildManager و سایر ابزارهای موجود در ASP.NET انجام خواهد شد.

نحوه استفاده از متد فوق نیز به صورت زیر است: 
var manager = CreateLocalResourceManager("~/Default.aspx");
Label1.Text = manager.GetString("Label1.Text");
 
نکته: ارائه و شرح کدهای پیاده‌سازی‎‌های پیش‌فرض برای آشنایی با نحوه صحیح سفارشی سازی این کلاس‌ها آورده شده است. پس با دقت بیشتر بر روی این کدها سعی کنید نحوه پیاده‌سازی مناسب را برای سفارشی‌سازی موردنظر خود پیدا کنید.

تا اینجا با مقدمات فرایند تولید پرووایدرهای سفارشی برای استفاده در فرایند بارگذاری ورودی‌های Resourceها آشنا شدیم. در ادامه به بحث تولید پرووایدرهای سفارشی برای استفاده از دیگر انواع منابع (به غیر از فایل‌های resx.) خواهم پرداخت.

منابع:
نظرات مطالب
کار با SignalR Core از طریق یک کلاینت Angular
دقیقا چه خطایی دریافت کردید؟ چون مشکلی نیست:

با این شرایط البته:

- سرویس مدنظر پیشتر در فایل آغازین برنامه ثبت شده باشد:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();
}
- همچنین هاب هم ثبت شده باشد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseSignalR(routes =>
            {
                routes.MapHub<MessageHub>(path: "/message");
            });
این ثبت هست که کلاس هاب را وارد سیستم تزریق وابستگی‌های ASP.NET Core می‌کند.