مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کرده‌اید .
یک مثال دیگر آن می‌تواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمی‌کند؛ اما ... زمانیکه نوبت به نمایش آن فرا می‌رسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace EFGeneral
{
    public class BlogComment
    {
        public int Id { set; get; }

        [MaxLength]
        public string Body { set; get; }

        public virtual BlogComment Reply { set; get; }
        public int? ReplyId { get; set; }
        public ICollection<BlogComment> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<BlogComment> BlogComments { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<BlogComment>()
                        .HasOptional(x => x.Reply)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ReplyId)
                        .WillCascadeOnDelete(false);

            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var comment1 = new BlogComment { Body = "نظر من این است که" };
            var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 };
            var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 };

            context.BlogComments.Add(comment121);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var list = ctx.BlogComments
                          //.where ...
                          .ToList() // fills the childs list too
                          .Where(x => x.Reply == null) // for TreeViewHelper                        
                          .ToList();

                if (list.Any())
                {

                }
            }
        }
    }
}

در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره می‌کند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه می‌کنید تشکیل خواهند شد:


یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.

اولین نکته اضافه‌‌تری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر می‌باشد:
    public class BlogComment
    {
        // ... 
        public ICollection<BlogComment> Children { get; set; }
    }
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر می‌گردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب می‌شود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت می‌شود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشه‌ها فیلتر می‌شوند (مواردی که reply آن‌ها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:


سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آن‌ها در یک برنامه ASP.NET MVC می‌توان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty)
        {
            var children = childrenProperty(parentItem);
            if (children == null || !children.Any())
            {
                return;
            }
//...

 
مطالب
بهبودهای کار با Lambdas در C# 9.0
C# 9.0 به همراه دو بهبود جزئی درباره‌ی کار با Lambdas است:
- امکان عدم ذکر بعضی از پارامترهای Lambdas
- امکان تعریف متدهای static anonymous


امکان عدم ذکر بعضی از پارامترهای Lambdas در C# 9.0

مثال زیر را در نظر بگیرید:
Action<object, EventArgs> handler1 = (object obj, EventArgs args) => ShowDialog();
در عبارت lambda تعریف شده، از پارامترهای obj و args استفاده نشده‌است. به همین جهت برای کاهش اخطارهای نمایش داده شده‌ی توسط کامپایلر در C# 9.0 می‌توان آن‌ها را به صورت discard parameters توسط _ معرفی کرد؛ به یکی از دو روش زیر:
Action<object, EventArgs> handler2 = (object _, EventArgs _) => ShowDialog();
// OR
Action<object, EventArgs> handler3 = (_, _) => ShowDialog();
که در یکی، نوع‌ها به همراه discard parameters ذکر شده‌اند و در دومی فقط discard parameters را داریم.

نمونه‌ی دیگر آن در حین تعریف رخ‌دادگردان‌ها است:
button1.Click += (s, e) => ShowDialog();
که اینبار پارامترهای استفاده نشده به صورت زیر قابل بیان هستند:
 button1.Click += (_, _) => ShowDialog();


امکان تعریف Static anonymous functions در C# 9.0

برای کاهش میزان تخصیص حافظه‌ی در حین کار با عبارات lambda ای که از متغیرهای محلی استفاده می‌کنند، اینبار در C# 9.0 می‌توان این عبارات را static تعریف کرد. برای نمونه مثال زیر را درنظر بگیرید:
namespace CS9Features
{
    public class LambdasTests
    {
        public void Test()
        {
            string text = "{0} is a good user !";
            PrintItems(item => string.Format(text, item));
        }

        private void PrintItems(Func<string, string> formatFunc)
        {
            foreach (var item in new[] { "User 1", "User 2" })
            {
                Console.WriteLine(formatFunc(item));
            }
        }
    }
}
در اینجا نحوه‌ی فرمت نهایی اطلاعات نمایش داده شده، توسط یک عبارت lambda تامین می‌شود. اتفاقی که در اینجا رخ می‌دهد، اصطلاحا capture شدن یک متغیر (text در اینجا) توسط یک anonymous function است (همان عبارت lambda نوشته شده). حاصل این capture شدن، ایجاد یک شیء موقتی برای مدیریت آن است که تخصیص حافظه و همچنین سربار عملیاتی اضافه‌ای را به همراه دارد. برای حذف این سربارها در C# 9.0 می‌توان متغیر استفاده شده را const تعریف کرد و سپس عبارت lambda تعریف شده را به صورت static نوشت:
const string text = "{0} is a good user !";
PrintItems(static item => string.Format(text, item));
با تعریف شدن یک عبارت lambda و یا یک anonymous method به صورت static که از تخصیص حافظه‌ی اضافی ایجاد شیء موقتی مدیریت کننده‌ی دسترسی به متغیر text جلوگیری می‌کند، اتفاقات زیر نیز رخ می‌دهند:
- این متد به عنوان static anonymous function شناخته می‌شود.
- دیگر نمی‌تواند دسترسی به حالت scope جاری را داشته باشد. بنابراین دیگر دسترسی به متغیرها، پارامترها و حتی شیء this را هم نخواهد داشت.
- می‌تواند با سایر اعضای استاتیک scope جاری کار کند.
- می‌تواند به تعاریف const مربوط به scope جاری، دسترسی داشته باشد.
مطالب
چرا در جاوااسکریپت نیازی به استفاده از loopها ندارید!
در زبان‌های برنامه نویسی، از loopها برای پیمایش عناصر یک مجموعه استفاده میشود. این پیمایش ممکن است صرفا جهت نمایش و یا دستکاری نمودن عناصر مجموعه، مورد استفاده قرار بگیرد (دستوراتی نظیر for,while,do while).
کد زیر را در نظر بگیرید که در آن قصد داریم عناصر مجموعه‌ای را تبدیل به حروف بزرگ کنیم. اینکار به طور معمول با استفاده از  loopهای معمول، به شکل زیر انجام میشود:
let names = ["Jack", "Jecci", "Ram", "Tom"];
let upperCaseNames = [];
for(let i=0, totalNames = names.length; i< totalNames ; i= i +1) {
    upperCaseNames = names[i].toUpperCase();
}
در  ECMAScript 5 توابعی معرفی شد که میتوانیم از آنها بجای loopها استفاده کنیم.
let names = ["Jack", "Jecci", "Ram", "Tom"];
let upperCaseNames = names.map(name => name.toUpperCase());
تابع map یک آرایه‌ی جدید را بازگشت می‌دهد (توسعه دهندگان React Js  از این تابع استفاده زیادی دارند). وقتی تابع map را بکارمیبرید، قابلیت استفاده از دستوراتی مانند  break , continue و return را نخواهید داشت و در صورت نیاز میتوانید از توابعی همچون some و یا every استفاده نمایید. 
مثال دیگری را در نظر بگیرید که در آن قصد داریم یک action  و عملی را روی عناصر مجموعه‌ای اعمال کنیم. در اینجا چاپ عناصر در نظر گرفته شده‌است:
function print(name) {
   console.log(name);
}
let names = ["Jack", "Jecci", "Ram", "Tom"];
for(let i=0, totalNames = names.length; i< totalNames ; i= i +1) {
    print(names[i])
}
با استفاده از تابع forEach، کد فوق بصورت زیر بازنویسی و خلاصه نویسی می‌شود:
function print(name) {
   console.log(name);
}
let names = ["Jack", "Jecci", "Ram", "Tom"];
names.forEach(name=> print(name));

//  اگر صرفا چاپ در کنسول مد نظر هست

let names = ["Jack", "Jecci", "Ram", "Tom"];
names.forEach(name=> console.log(name));


در مثالی دیگر قصد فیلتر کردن عناصر مجموعه‌ای را بر اساس شرطی خاص داریم (عناصر فرد):
function isOdd(n) {
   return n %2;
}
let numbers = [1,2,3,4,5];
let odd = [];
for(let i=0, total = numbers.length; i< total ; i= i +1) {
   let number = numbers[i];
   if( isOdd(number) ) {
      odd.push(number);
   }
}
با استفاده از تابع filter، کد فوق را بهبود می‌دهیم:
let numbers = [1,2,3,4,5, 6, 7]
let odd = numbers.filter(n => n%2);
فرض کنید قرار است یک خروجی را بعد از پیمایش عناصر مجموعه‌ای داشته باشیم (مجموع عناصر):
let numbers = [1,2,3,4,5]
let result = 0;
for(let i=0, total = numbers.length; i< total ; i= i +1) {
   result = result + numbers[i];
}
با استفاده از تابع  reduce، کد فوق را بازنویسی میکنیم:
let numbers = [1,2,3,4,5,6,7];
function sum(accumulator, currentValue){
   return accumulator + currentValue;
}
let initialVal = 0;
let result = numbers.reduce(sum, initialVal);

// یا بصورت زیر کد را خلاصه نمود

let numbers = [1,2,3,4,5,6,7, 10];
let result = numbers.reduce((acc, val)=> acc+val, 0);

در مثال دیگری قصد بررسی این مورد را داریم که آیا  آرایه‌ای، دارای مقدار خاصی می‌باشد و در صورت دارا بودن آن آیتم، از ادامه پیمایش خارج شود:
let names = ["ram",, "rahul", "raj", "rahul"];
for(let i=0, totalNames = names.length; i< totalNames ; i= i +1) {
   if(names[i] === "rahul") {
     console.log(" found rahul");
     break; 
   }
}
اکنون با استفاده از تابع some، بصورت زیر کد را بهبود میدهیم: 
let names = ["ram",, "rahul", "raj", "rahul"];
let isRahulPresent = names.some(name => name==="rahul");
if(isRahulPresent) {
  console.log("found rahul"); 
}

در یک آرایه قصد داریم شرطی را بر روی همه‌ی عناصر آن چک کنیم و در صورت صحت شرط، بر روی کل مجموعه، action یا عملی را انجام دهیم:
let numbers = [1,2,3,4,5, 0];
for(let i=0, total = numbers.length; i< total ; i= i +1) {
    if(numbers[i] <= 0) {
      console.log("0 present in array");
      break;
     }
}

این کد را با استفاده از تابع every، بصورت زیر بازنویسی میکنیم:
let numbers = [1,2,3,4,5,0];
let isZeroFree = numbers.every(e => e > 0);
if(!isZeroFree) {
    console.log("0 present in array");
}

نتیجه گیری:
 استفاده از این نوع توابع، مزیت‌های زیر را به همراه دارد:
  • خوانایی بهتر
  • فهم راحت
  • خطایابی آسان‌تر
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 2 - ثبت اولین سرویس
یک پروژه‌ی ASP.NET Core را با قرار دادن نسخه‌ی NET Core. بر روی 3.1 و با استفاده از قالب Model View Controller ایجاد کنید. در اینجا نام پروژه را AspNetCoreDependencyInjection گذاشته‌ام. حالا در  پوشه‌ی Models، فایلی را با نام HomeViewModel.cs با محتویاتی به صورت زیر اضافه کنید:
public class HomeViewModel
{
     public string Id { get; set; }
     public string Message { get; set; }
     public DateTime DateTime { get; set; }
}

اکنون به پوشه‌ی Views بروید و فایل Index.cshtml را به این صورت تغییر دهید:

@model AspNetCoreDependencyInjection.Models.HomeViewModel
@{
ViewData["Title"] = "Home";
}

<div>
<div>
<div>
<p>
<b>Id : </b><span>@Model.Id</span> <br />
<b>Date And Time : </b><span> @Model.DateTime </span> <br/>
<b>Message : </b><span>@Model.Message</span>
</p>
</div>
</div>
</div>
و فایل MessageServiceA.cs را به پروژه اضافه کنید:
using AspNetCoreDependencyInjection.Services;

namespace AspNetCoreDependencyInjection.ServicesImplentaions
{
    public class MessageServiceAA 
    {
        public string Message()
        {
            return "A message from MessageServiceAA";
        }
    }
}
و همچنین فایل GuidHelper.cs را نیز اضافه می‌کنیم:
namespace AspNetCoreDependencyInjection.Helpers
{
    public class GuidProvider
    {
        private readonly Guid _serviceGuid;

        public GuidProvider()
        {
            _serviceGuid = Guid.NewGuid();
        }

        public Guid GetNewGuid() => Guid.NewGuid();

        public string GetGuidAsFormatedString(string prefix = "") => getFormatedGuid(_serviceGuid, prefix);

        private string getFormatedGuid(Guid guid, string prefix = "")
        {
            var guidString = guid.GetHashCode().ToString("x");
            if (string.IsNullOrEmpty(prefix) == false)
                guidString = new StringBuilder($"{prefix}-").Append(guidString).ToString();
            return guidString;
        }
    }
}

حالا درون کنترل HomeController، این تغییرات را انجام می‌دهیم:

private readonly ILogger<HomeController> _logger;
private readonly MessageServiceAA _messageService;
private readonly GuidProvider _ guidProvider;

public HomeController(ILogger<HomeController> logger)
{
            _logger = logger;
            _messageService = new MessageServiceAA();
            _guidProvider = new GuidProvider();
}

public IActionResult Index()
{
            var model = new HomeViewModel()
            {
                Id = _ guidProvider.GetGuidAsFormatedString(),
                Message = _messageService.Message(),
                DateTime = DateTime.Now,
            };
            return View(model);
}

همانطور که می‌بینید، در کد بالا، کنترلر HomeController، به دو شیء از کلاس‌ها و یا سرویس‌های GuidProvider و MessageServiceAA به صورت مستقیم وابسته شده‌است و با هر تغییری در هر کدام از این سرویسها، باید دوباره کامپایل شود. علاوه بر این اگر بخواهیم پیاده سازی‌های مختلفی را برای هر کدام از این موارد، ارائه دهیم، به مشکل بر می‌خوریم. خب بیاید تغییراتی را در کد بالا بدهیم تا مشکلات ذکر شده را حل کنیم.

برای این منظور پوشه‌ای را به نام Services می‌سازیم و اینترفیسی را به نام IMessageBrokerA ایجاد می‌کنیم و سپس کاری می‌کنیم که MessageServiceAA از این اینترفیس ارث بری کند:

namespace AspNetCoreDependencyInjection.Services
{
    public interface IMessageServiceA
    {
        string Message();
    }
}

و حالا می‌خواهیم با استفاده از تزریق وابستگی، وابستگی کنترلر HomeController را از کلاس MessageBrokerAA لغو کرده و آن را به اینترفیس IMessageBrokerA (انتزاع) وابسته کنیم. در اینجا ما از تکنیک تزریق درون سازنده یا Constructor Injection استفاده می‌کنیم.


تزریق درون سازنده

در این تکنیک، ما لیستی از وابستگی‌های مورد نیاز را به عنوان پارامترهای ورودی سازنده‌ی کلاس، تعریف می‌کنیم:
private readonly ILogger<HomeController> _logger;
private readonly IMessageServiceA _messageService;
private readonly GuidProvider _guidHelper;
public HomeController(ILogger<HomeController> logger , IMessageServiceA messageService)
{
        _logger = logger;
        _messageService = messageService;
        _messageService = new MessageServiceAA();
        _guidHelper = new GuidProvider();
}
و حالا اگر برنامه را اجرا کنیم، با خطایی روبه رو می‌شویم که در آن می‌گوید امکان واکشی (Resolve) سرویس‌های مورد نظر وجود ندارد. این خطا به دلیل ثبت نشدن اینترفیس IMessageServiceA و پیاده سازی آن، درون Microsoft Dependency Injection Container است   DI Container‌ها معمولا باید در زمان شروع برنامه، پیکربندی و مقدار دهی شودند، تا در ادامه‌ی چرخه‌ی حیات برنامه، بتوانند سرویس‌ها و اشیاء مورد نیاز را به کلاس‌هایی که نیاز دارند، واکشی و تزریق کنند. اولین مرحله از کار با DI Container‌ها، ثبت کردن سرویس‌ها درون آنهاست.
در ASP.NET Core از IServiceCollection برای ثبت کردن سرویس‌های برنامه‌ی خودمان استفاده می‌کنیم. تمامی سرویس‌هایی را که انتظار داریم توسط DI Container به کلاس‌هایی تزریق شوند، باید درون IServiceCollection ثبت گردند. تمام سرویس‌هایی که به وسیله‌ی IServiceCollection ثبت شده‌اند، پس از ساخته شدن، توسط اینترفیس IServiceProvider قابل واکشی هستند.

بنابراین دو اینترفیس حیاتی برای کار کرد صحیح Microsoft Dependency Injection Container درون ASP.NET Core وجود دارند:
  • IServiceCollection : برای ثبت سرویس‌ها
  • IServiceProvider : برای واکشی سرویس‌ها

در ASP.NET Core معمول‌ترین مکان برای ثبت کردن سرویس‌ها درون Container، به صورت پیش فرض درون کلاس Startup و درون متد ConfigureServices انجام می‌گیرد.
به صورت پیش فرض کلاس Startup دو متد دارد:
  • ConfigureServices : برای پیکربندی و ثبت سرویس‌های درونی DI Container استفاده می‌شود.
  • Configure : برای تنظیمات pipeline میان افزارها ( Middlewares ) بکار می‌رود.

در اینجا پیاده سازی پیش فرض کلاس Startup را می‌بینیم که البته کدهای درون متد Configure را برای درگیر نکردن ذهن شما، مخفی کرده‌ایم: 
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //  کدها جهت خوانایی بیشتر مخفی شده اند
        }
    }

همانطورکه می‌بینید، متد ConfigureService پارامتر IServiceCollection را می‌گیرد که به وسیله‌ی WebHost در زمان اجرای برنامه، مقدار دهی می‌شود.

تعداد زیادی Extension method برای IServiceCollection وجود دارند که برای پشتیبانی از ثبت کردن سرویس‌های مختلف در سناریوهای گوناگون به کار می‌روند. در اینجا ما از نسخه‌ی 3.1 چارچوب ASP.NET Core استفاده می‌کنیم. برای همین هم برای ثبت سرویس‌های پیش فرض فریمورک MVC از متد توسعه‌ی services.AddControllersWithViews()    استفاده می‌کنیم.  متد توسعه‌ی AddControllersWithViews() سرویس‌هایی را که معمولا در فریم ورک MVC استفاده می‌شوند، درون IServiceCollection ثبت می‌کند. در نسخه‌های قبلی چارچوب ASP.NET Core،  مانند نسخه‌های 2.1 و 2.2 برای این کار از متد توسعه‌ی AddMvc() استفاده می‌شد.

در Microsoft Dependency Injection Container ، معمولا  ترتیب ثبت سرویس‌ها مهم نیست.

خب، اولین سرویس اختصاصی برنامه‌ی خودمان را با چرخه‌ی حیات Transient و زیر سرویس پیشین، به شکل زیر ثبت می‌کنیم :

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddTransient<IMessageServiceA, MessageServiceAA>();
        }
همانطور که می‌بینید، در اینجا ما از متد AddTransient() استفاده کرده‌ایم. متد AddTransient() درون فضای نام Microsoft.Extensions.DependencyInjection قرار دارد. این متد Overload ‌های گوناگونی دارد و ما از نوعی از آن استفاده کرده‌ایم که دو نوع generic را می‌پذیرد و تعریف آن به صورت زیر است: 
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
در اینجا TService ، اینترفیس سرویس ماست. این نوع، همان نوعی است که کلاس‌های ما می‌توانند به آن وابسته باشند. پارامتر دوم، از نوع TImplemention است که پیاده سازی مورد نظر برای TService را ثبت می‌کند. TImplmention   نوعی است که Container در زمان واکشی و تزریق TService از آن نمونه سازی کرده و به کلاس مورد نظر تزریق می‌کند.

در اینجا وقتی ما برای IMessageServiceA ، پیاده سازی MessageServiceA را ثبت می‌کنیم، از این به بعد DI Container، هر زمانیکه در لیست پارامترهای سازنده‌ی یک کلاس، IMessageServiceA را مشاهده کند، بررسی می‌کند که چه کلاسی به عنوانی پیاده سازی این اینترفیس ثبت شده‌است، سپس از آن نمونه سازی می‌کند و درون سازنده‌ی مورد نظر تزریق می‌کند. خب، حالا برنامه را دوباره اجرا کنید؛ می‌بینید که برنامه اجرا می‌شود.

 
در ادامه ابتدا در مورد روش‌های مختلف ثبت سرویس‌ها و بعد روش‌های واکشی سرویس‌ها را بررسی می‌کنیم.
مطالب
استفاده از date picker شمسی جاوا اسکریپتی در Blazor با قابلیت ورود تاریخ به صورت دستی
دیت پیکرهای گوناگونی توسط افراد مختلف نوشته شده‌اند که هر یک مشکلات خاص خود را دارند. در این مطلب به چگونگی استفاده از یکی از سازگارترین  دیت پیکرهای جاوا اسکریپتی که توسط آقای امیرمسعود ایرانی نوشته شده است در Blazor خواهیم پرداخت.
مهم‌ترین ویژگی این دیت پیکر امکان ورود تاریخ به صورت دستی توسط کاربر است.

فرمت‌های قابل قبول برای ورود تاریخ عبارتند از:
۹۰۰۸۱۴ ۱۴۰۸۹۰ ۱۳۹۰۰۸۱۴ ۱۴/۸/۹۰ ۹۰/۸/۱۴ ۱۴/۸/۱۳۹۰ ۱۳۹۰/۸/۱۴ ۱۴-۸-۹۰ ۹۰-۸-۱۴ ۱۴-۸-۱۳۹۰ ۱۳۹۰-۸-۱۴ 
و فرمت‌های ویژه:
۰۸۱۴ ۱۴۰۸ ۱۴-۸ ۸-۱۴ ۱۴/۸ ۸/۱۴ ۱۴
در فرمت‌های ویژه که سال و ماه وارد نشده‌اند، سال و ماه فعلی به حساب خواهد آمد.
در فرمت‌هایی که سال مشخص نشده باشد، دو رقم ابتدایی در صورت امکان روز محاسبه خواهند شد.
بنابراین قادر خواهیم بود که در خروجی یک فرمت استاندارد داشته باشیم حتی با فرمت‌های مختلفی که کاربر وارد خواهد کرد.

روش به کارگیری تقویم در Blazor

در ابتدا فایل‌های مورد نیاز را دانلود کرده (AMIB_jsPersianCal_0.2.1.rar) و به پروژه اضافه می‌کنیم.
سپس به _layout رفته و ارجاعات زیر را برای افزودن فایل‌های css و js به پروژه اضافه می‌کنیم:
<link href="css/js-persian-cal.css" rel="stylesheet"/>
<script src="js/js-persian-cal.min.js"></script>
حال برای استفاده از دیت پیکر در کامپوننت‌ها از تگ input به شکل زیر استفاده می‌کنیم:
<input type="text" id="pcal1" />
Id آن مهم است زیرا توسط آن به تابع جاوااسکریپتی معرفی می‌شود. می‌توان هر اسمی را اختیار کرد فقط بهتر است تمامی دیت پیکرهای موجود در صفحه یک اسم داشته باشند اما با ایندکس‌های مختلف مانند pcal1، pcal2 و ... . دلیل آن این است که می‌توان تمامی دیت پیکرهای را توسط یک حلقه به تابع مربوطه معرفی کرد.
همانطور که می‌دانید برای استفاده از توابع جاوا اسکریپتی در Blazor از JSRuntime استفاده می‌شود. بنابراین به شکل زیر عمل خواهیم کرد.
protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        int dateFieldCount = 1;
        if (firstRender)
        {
            for (int i = 1; i <= dateFieldCount; i++)
            {
                await JsRuntime.InvokeVoidAsync("CallAmib", "pcal" + i.ToString());
            }
        }
    }
توسط حلقه for تمامی تگ‌های input موجود در کامپوننت را که Id آنها با pcal شروع می‌شود به دیت پیکر تبدیل خواهیم نمود. فقط مقدار متغیر dateFieldCount را باید به تعداد تگ‌های دیت پیکر موجود در کامپوننت تنظیم نمود.
لازم به ذکر است که باید در ابتدای کامپوننت، JSRuntime را به شکل زیر تزریق نمود.
@inject IJSRuntime  JsRuntime
حال فقط کافیست اسکریپت CallAmib را ایجاد کرده و به _layout اضافه نمود.
window.CallAmib = (objCal1) => {
    new AMIB.persianCalendar(objCal1);
}
  بنابراین فایل _layout برنامه الان چیزی شبیه به زیر خواهد بود:
@using Microsoft.AspNetCore.Components.Web
@namespace ShamsiDatePickerBlazor.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="css/js-persian-cal.css" rel="stylesheet" />
    <component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
<body>
    @RenderBody()

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="">Reload</a>
        <a>🗙</a>
    </div>
    <script src="js/js-persian-cal.min.js"></script>
    <script src="js/CallAmib.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>
</html>
تا اینجای کار اگر پروژه را اجرا کنیم، دیت پیکری مانند زیر را خواهیم داشت:

مشکل!!

برای بایند کردن مقدار تاریخ انتخاب شده نمی‌توان از bind-value به طور معمول استفاده کرد؛ زیرا در حقیقت تغییرات input با جاوا اسکریپت انجام می‌گیرد و حالت صفحه تغییری نمی‌کند. برای مرتفع کردن این مشکل نیاز است که در اسکریپت CallAmib متد onchange به شکل زیر صدا زده شده و مقدار تاریخ انتخابی به یک متد داخل کامپوننت ارسال گردیده و در آنجا به یک فیلد منتسب شود.
window.CallCall = (objCal1) => {
    new AMIB.persianCalendar(objCal1,{
        onchange: function(pdate) {
            DotNet.invokeMethodAsync('ShamsiDatePickerBlazor', 'DateChanged', pdate.toString()).then(
                (date) => {
                    console.log(data);
                }
            );
        }
    });
}
توضیحات اسکریپت بالا:
متغیر pdate به صورت توکار مربوط به AMIB.persianCalendar می باشد و مقدار تاریخ انتخابی را در بر دارد.
متد DotNet.invokeMethodAsync یک متد توکار دات نت می‌باشد و برای فراخوانی متدهای سی شارپی از داخل توابع جاوا اسکریپتی به کار می‌رود. آرگومان اول آن در حقیقت نام اسمبلی پروژه می‌باشد. آرگومان دوم آن نام تابع سی شارپی‌است که باید فراخوانی شود و در نهایت آرگومان سوم آن تاریخ انتخاب شده می‌باشد.
در پایان باید متد DateChanged،  به شکل زیر در کامپوننت index نوشته شود:
static string selectedDate;
[JSInvokable]
public static void DateChanged(string pdate)
{
    selectedDate = pdate;
}
این تابع بایستی با صفت [JSInvokable] مزین شود و حتما هم استاتیک باشد.
برای دیدن مقدار جدید selectedDate کافی است روی دکمه ShowNewValue یکبار کلیک نمایید.
نکته: می‌توان به جای input، از InputText مربوط به EditForm هم استفاده نمود. فقط باید یک Id هم به آن انتساب داد. همچنین برای انتساب مقدار دیت پیکر به مدل، باید در متد DateChanged، فیلد مورد نظر از مدل را بجای متغیر selectedDate گذاشت.
شما می‌توانید در اینجا کدهای کامل این مطلب را ملاحظه نمایید.
مطالب دوره‌ها
استفاده از AOP Interceptors برای حذف کدهای تکراری کش کردن اطلاعات در لایه سرویس برنامه
اکثر برنامه‌های ما دارای قابلیت‌هایی هستند که با موضوعاتی مانند امنیت، کش کردن اطلاعات، مدیریت استثناها، ثبت وقایع و غیره گره خورده‌اند. به هر یک از این موضوعات یک Aspect یا cross-cutting concern نیز گفته می‌شود.
در این قسمت قصد داریم اطلاعات بازگشتی از لایه سرویس برنامه را کش کنیم؛ اما نمی‌خواهیم مدام کدهای مرتبط با کش کردن اطلاعات را در مکان‌های مختلف لایه سرویس پراکنده کنیم. می‌خواهیم یک ویژگی یا Attribute سفارشی را تهیه کرده (مثلا به نام CacheMethod) و به متد یا متدهایی خاص اعمال کنیم. سپس برنامه، در زمان اجرا، بر اساس این ویژگی‌ها، خروجی‌های متدهای تزئین شده با ویژگی CacheMethod را کش کند.
در اینجا نیز از ترکیب StructureMap و DynamicProxy پروژه Castle، برای رسیدن به این مقصود استفاده خواهیم کرد. به کمک StructureMap می‌توان در زمان وهله سازی کلاس‌ها، آن‌ها را به کمک متدی به نام EnrichWith توسط یک محصور کننده دلخواه، مزین یا غنی سازی کرد. این مزین کننده را جهت دخالت در فراخوانی‌های متدها، یک DynamicProxy درنظر می‌گیریم. با پیاده سازی اینترفیس IInterceptor کتابخانه DynamicProxy مورد استفاده و تحت کنترل قرار دادن نحوه و زمان فراخوانی متدهای لایه سرویس، یکی از کارهایی را که می‌توان انجام داد، کش کردن نتایج است که در ادامه به جزئیات آن خواهیم پرداخت.


پیشنیازها

ابتدا یک برنامه جدید کنسول را آغاز کنید. تنظیمات آن‌را از حالت Client profile به Full تغییر دهید.
سپس همانند قسمت‌های قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
 PM> Install-Package structuremap
PM> Install-Package Castle.Core
همچنین ارجاعی را به اسمبلی استاندارد System.Web.dll نیز اضافه نمائید.
از این جهت که از HttpRuntime.Cache قصد داریم استفاده کنیم. HttpRuntime.Cache در برنامه‌های کنسول نیز کار می‌کند. در این حالت از حافظه سیستم استفاده خواهد کرد و در پروژه‌های وب از کش IIS بهره می‌برد.


ویژگی CacheMethod مورد استفاده

using System;

namespace AOP02.Core
{
    [AttributeUsage(AttributeTargets.Method)]
    public class CacheMethodAttribute : Attribute
    {
        public CacheMethodAttribute()
        {
            // مقدار پیش فرض
            SecondsToCache = 10;
        }

        public double SecondsToCache { get; set; }
    }
}
همانطور که عنوان شد، قصد داریم متدهای مورد نظر را توسط یک ویژگی سفارشی، مزین سازیم تا تنها این موارد توسط AOP Interceptor مورد استفاده پردازش شوند.
در ویژگی CacheMethod، خاصیت SecondsToCache بیانگر مدت زمان کش شدن نتیجه متد خواهد بود.


ساختار لایه سرویس برنامه

using System;
using System.Threading;
using AOP02.Core;

namespace AOP02.Services
{
    public interface IMyService
    {
        string GetLongRunningResult(string input);
    }

    public class MyService : IMyService
    {
        [CacheMethod(SecondsToCache = 60)]
        public string GetLongRunningResult(string input)
        {
            Thread.Sleep(5000); // simulate a long running process
            return string.Format("Result of '{0}' returned at {1}", input, DateTime.Now);
        }
    }
}
اینترفیس IMyService و پیاده سازی نمونه آن‌را در اینجا مشاهده می‌کنید. از این لایه در برنامه استفاده شده و قصد داریم نتیجه بازگشت داده شده توسط متدی زمانبر را در اینجا توسط AOP Interceptors کش کنیم.


تدارک یک CacheInterceptor

using System;
using System.Web;
using Castle.DynamicProxy;

namespace AOP02.Core
{
    public class CacheInterceptor : IInterceptor
    {
        private static object lockObject = new object();

        public void Intercept(IInvocation invocation)
        {
            cacheMethod(invocation);
        }

        private static void cacheMethod(IInvocation invocation)
        {
            var cacheMethodAttribute = getCacheMethodAttribute(invocation);
            if (cacheMethodAttribute == null)
            {
                // متد جاری توسط ویژگی کش شدن مزین نشده است
                // بنابراین آن‌را اجرا کرده و کار را خاتمه می‌دهیم
                invocation.Proceed();
                return;
            }

            // دراینجا مدت زمان کش شدن متد از ویژگی کش دریافت می‌شود
            var cacheDuration = ((CacheMethodAttribute)cacheMethodAttribute).SecondsToCache;

            // برای ذخیره سازی اطلاعات در کش نیاز است یک کلید منحصربفرد را
            //  بر اساس نام متد و پارامترهای ارسالی به آن تهیه کنیم
            var cacheKey = getCacheKey(invocation);

            var cache = HttpRuntime.Cache;
            var cachedResult = cache.Get(cacheKey);


            if (cachedResult != null)
            {
                // اگر نتیجه بر اساس کلید تشکیل شده در کش موجود بود
                // همان را بازگشت می‌دهیم
                invocation.ReturnValue = cachedResult;
            }
            else
            {
                lock (lockObject)
                {
                    // در غیر اینصورت ابتدا متد را اجرا کرده
                    invocation.Proceed();
                    if (invocation.ReturnValue == null)
                        return;

                    // سپس نتیجه آن‌را کش می‌کنیم
                    cache.Insert(key: cacheKey,
                                 value: invocation.ReturnValue,
                                 dependencies: null,
                                 absoluteExpiration: DateTime.Now.AddSeconds(cacheDuration),
                                 slidingExpiration: TimeSpan.Zero);
                }
            }
        }

        private static Attribute getCacheMethodAttribute(IInvocation invocation)
        {
            var methodInfo = invocation.MethodInvocationTarget;
            if (methodInfo == null)
            {
                methodInfo = invocation.Method;
            }
            return Attribute.GetCustomAttribute(methodInfo, typeof(CacheMethodAttribute), true);
        }

        private static string getCacheKey(IInvocation invocation)
        {
            var cacheKey = invocation.Method.Name;

            foreach (var argument in invocation.Arguments)
            {
                cacheKey += ":" + argument;
            }

            // todo: بهتر است هش این کلید طولانی بازگشت داده شود
            // کار کردن با هش سریعتر خواهد بود
            return cacheKey;
        }
    }
}
کدهای CacheInterceptor مورد استفاده را در بالا مشاهده می‌کنید.
توضیحات ریز قسمت‌های مختلف آن به صورت کامنت، جهت درک بهتر عملیات، ذکر شده‌اند.


اتصال Interceptor به سیستم

خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شده‌اند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آن‌ها مطلع کنیم.
using System;
using AOP02.Core;
using AOP02.Services;
using Castle.DynamicProxy;
using StructureMap;

namespace AOP02
{
    class Program
    {
        static void Main(string[] args)
        {
            ObjectFactory.Initialize(x =>
            {
                var dynamicProxy = new ProxyGenerator();
                x.For<IMyService>()
                 .EnrichAllWith(myTypeInterface =>
                        dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new CacheInterceptor()))
                 .Use<MyService>();
            });

            var myService = ObjectFactory.GetInstance<IMyService>();
            Console.WriteLine(myService.GetLongRunningResult("Test"));
            Console.WriteLine(myService.GetLongRunningResult("Test"));
        }
    }
}
در قسمت تنظیمات اولیه DI مورد استفاده، هر زمان که شیءایی از نوع IMyService درخواست شود، کلاس MyService وهله سازی شده و سپس توسط CacheInterceptor محصور می‌گردد. اکنون ادامه برنامه با این شیء محصور شده کار می‌کند.
حال اگر برنامه را اجرا کنید یک چنین خروجی قابل مشاهده خواهد بود:
 Result of 'Test' returned at 2013/04/09 07:19:43
Result of 'Test' returned at 2013/04/09 07:19:43
همانطور که ملاحظه می‌کنید هر دو فراخوانی یک زمان را بازگشت داده‌اند که بیانگر کش شدن اطلاعات اولی و خوانده شدن اطلاعات فراخوانی دوم از کش می‌باشد (با توجه به یکی بودن پارامترهای هر دو فراخوانی).

از این پیاده سازی می‌شود به عنوان کش سطح دوم ORMها نیز استفاده کرد (صرفنظر از نوع ORM در حال استفاده).

دریافت مثال کامل این قسمت
AOP02.zip
مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
پس از آشنایی مقدماتی با Kendo UI DataSource، اکنون می‌خواهیم از آن جهت صفحه بندی، مرتب سازی و جستجوی پویای سمت سرور استفاده کنیم. در مثال قبلی، هر چند صفحه بندی فعال بود، اما پس از دریافت تمام اطلاعات، این اعمال در سمت کاربر انجام و مدیریت می‌شد.



مدل برنامه

در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
namespace KendoUI03.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}


پیشنیاز تامین داده مخصوص Kendo UI Grid

برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواست‌های صفحه بندی خود را با فرمت ذیل ارسال می‌کند. همانطور که مشاهده می‌کنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
 /api/products?{"take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
سپس این گرید نیاز به سه فیلد، در خروجی JSON نهایی خواهد داشت:
{
"Data":
[
{"Id":1500,"Name":"نام 1500","Price":2499.0,"IsAvailable":false},
{"Id":1499,"Name":"نام 1499","Price":2498.0,"IsAvailable":true}
],
"Total":1500,
"Aggregates":null
}
فیلد Data که رکوردهای گرید را تامین می‌کنند. فیلد Total که بیانگر تعداد کل رکوردها است و Aggregates که برای گروه بندی بکار می‌رود.

می‌توان برای تمام این‌ها، کلاس و Parser تهیه کرد و یا ... پروژه‌ی سورس بازی به نام  Kendo.DynamicLinq نیز چنین کاری را میسر می‌سازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
 PM> Install-Package Kendo.DynamicLinq
Kendo.DynamicLinq به صورت خودکار System.Linq.Dynamic را نیز نصب می‌کند که از آن جهت صفحه بندی پویا استفاده خواهد شد.


تامین کننده‌ی داده سمت سرور

همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابی‌های مخصوص آن‌را به فایل global.asax.cs نیز اضافه نمائید.
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using Kendo.DynamicLinq;
using KendoUI03.Models;
using Newtonsoft.Json;

namespace KendoUI03.Controllers
{
    public class ProductsController : ApiController
    {
        public DataSourceResult Get(HttpRequestMessage requestMessage)
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
                requestMessage.RequestUri.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter);
        }
    }
}
تمام کدهای این کنترلر همین چند سطر فوق هستند. با توجه به ساختار کوئری استرینگی که در ابتدای بحث عنوان شد، نیاز است آن‌را توسط کتابخانه‌ی JSON.NET تبدیل به یک نمونه از DataSourceRequest نمائیم. این کلاس در Kendo.DynamicLinq تعریف شده‌است و حاوی اطلاعاتی مانند take و skip کوئری LINQ نهایی است.
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده می‌شود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شده‌ی Data، Total و Aggregates را تولید می‌کند.

تا اینجا کارهای سمت سرور این مثال به پایان می‌رسد.


تهیه View نمایش اطلاعات ارسالی از سمت سرور

اعمال مباحث بومی سازی
<head>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Language" content="fa" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <!--شیوه نامه‌ی مخصوص راست به چپ سازی-->
    <link href="styles/kendo.rtl.min.css" rel="stylesheet" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>

    <!--محل سفارشی سازی پیام‌ها و مسایل بومی-->
    <script src="js/cultures/kendo.culture.fa-IR.js" type="text/javascript"></script>
    <script src="js/cultures/kendo.culture.fa.js" type="text/javascript"></script>
    <script src="js/messages/kendo.messages.en-US.js" type="text/javascript"></script>

    <style type="text/css">
        body {
            font-family: tahoma;
            font-size: 9pt;
        }
    </style>

    <script type="text/javascript">
        // جهت استفاده از فایل: kendo.culture.fa-IR.js
        kendo.culture("fa-IR");
    </script>
</head>
- در اینجا چند فایل js و css جدید اضافه شده‌اند. فایل kendo.rtl.min.css جهت تامین مباحث RTL توکار Kendo UI کاربرد دارد.
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شده‌اند. فایل‌های fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شده‌اند، اما نام ماه‌های موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایل‌ها، جهت ویرایش نهایی نیاز خواهد بود که در پوشه‌ی src\js\cultures مجموعه‌ی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیام‌های مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا می‌توانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شده‌است. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آن‌را به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.

راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
    <div class="k-rtl">
        <div id="report-grid"></div>
    </div>
k-rtl و تنظیمات آن در فایل kendo.rtl.min.css قرار دارند که در ابتدای head صفحه تعریف شده‌است.

تامین داده و نمایش گرید

در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه می‌کنید:
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
                schema: {
                    data: "Data",
                    total: "Total",
                    model: {
                        fields: {
                            "Id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string" },
                            "IsAvailable": { type: "boolean" },
                            "Price": { type: "number" }
                        }
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                pageSize: 10,
                sort: { field: "Id", dir: "desc" },
                serverPaging: true,
                serverFiltering: true,
                serverSorting: true
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                filterable: true,
                reorderable: true,
                columnMenu: true,
                columns: [
                    { field: "Id", title: "شماره", width: "130px" },
                    { field: "Name", title: "نام محصول" },
                    {
                        field: "IsAvailable", title: "موجود است",
                        template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                    },
                    { field: "Price", title: "قیمت", format: "{0:c}" }
                ]
            });
        });
    </script>
- با تعاریف مقدماتی Kendo UI DataSource پیشتر آشنا شده‌ایم و قسمت read آن جهت دریافت اطلاعات از سمت سرور کاربرد دارد.
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم می‌گیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیف‌ها در کدام فیلد قرار گرفته‌است.
- نحوه‌ی تعریف model را نیز در اینجا ملاحظه می‌کنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمی‌توان عدد 1 رشته‌ای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شده‌اند. اگر این مقدار دهی‌ها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.

پس از تعریف DataSource، تنها کافی است آن‌را به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب می‌شود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام می‌کنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال می‌کند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستون‌ها با کلیک بر روی سرستون‌ها فعال می‌گردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانه‌ی Kendo.DynamicLinq حاصل آن‌را در سمت سرور مدیریت می‌کند.
- reorderable: true سبب می‌شود تا کاربر بتواند محل قرارگیری ستون‌ها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستون‌ها نیز مسیر خواهد شد.
- در آخر ستون‌های گرید مشخص شده‌اند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت می‌گردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI03.zip
مطالب
آشنایی با قابلیت FileStream اس کیوال سرور 2008 - قسمت سوم

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

alter table tbl_files set(filestream_on ='default')

go
alter table tbl_files
add

[systemfile] varbinary(max) filestream null ,
FileId uniqueidentifier not null rowguidcol unique default (newid())
go

در ادامه جدول tblFiles قسمت قبل را در نظر بگیرید:

CREATE TABLE [tblFiles](
[FileId] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[Title] [nvarchar](255) NOT NULL,
[SystemFile] [varbinary](max) FILESTREAM NULL,
UNIQUE NONCLUSTERED
(
[FileId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] FILESTREAM_ON [fsg1]

ALTER TABLE [dbo].[tblFiles] ADD DEFAULT (newid()) FOR [FileId]
GO

نحوه‌ی افزودن رکوردی جدید به جدول tblFiles :

INSERT INTO [tblFiles]
(
[Title],
[SystemFile]
)
VALUES
(
'file-1',
CAST('data data data' AS VARBINARY(MAX))
)
در اینجا سعی کرده‌ایم یک رشته ساده را در فیلدی از نوع فایل استریم ذخیره کنیم که روش کار به صورت فوق است. از آنجائیکه مقدار پیش فرض FileId را هنگام تعریف جدول به NEWID تنظیم کرده‌ایم، نیازی به ذکر آن نیست و به صورت خودکار محاسبه و ذخیره خواهد شد.
اگر کنجکاو باشید که این فایل اکنون کجا ذخیره شده و نحوه‌ی مدیریت آن توسط اس کیوال سرور به چه صورتی است، فقط کافی است به مسیری که هنگام افزودن گروه فایل‌ها و فایل مربوطه در تنظیمات خواص دیتابیس در قسمت قبل مشخص کردیم، مراجعه کرد (شکل زیر).



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

using System;
using System.IO;
using System.Data.SqlClient;
using System.Data;

namespace FileStreamTest
{
class CFS
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//ساخت عبارت افزودن پارامتری
using (SqlCommand objSqlCmd = new SqlCommand(
"INSERT INTO [tblFiles]([Title],[SystemFile]) VALUES(@title , @file)",
objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.Text;

//تعریف وضعیت پارامترها و مقدار دهی آن‌ها
objSqlCmd.Parameters.AddWithValue("@title", title);
objSqlCmd.Parameters.AddWithValue("@file", buffer);

//اجرای فرامین
objSqlCmd.ExecuteNonQuery();
}

//پایان تراکنش
objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] data = null;

using (SqlConnection objConn = new SqlConnection())
{
//کوئری اس کیوال پارامتری جهت دریافت محتویات فایل
string cmdText = "SELECT SystemFile FROM tblFiles WHERE FileId=@id";
using (SqlCommand objCmd = new SqlCommand(cmdText, objConn))
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objConn.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objConn.Open();

//تنظیم کردن وضعیت و مقدار پارامتر تعریف شده در کوئری
objCmd.Parameters.AddWithValue("@id", fileId);

//اجرای فرامین و دریافت فایل
using (SqlDataReader objread = objCmd.ExecuteReader())
{
if (objread != null)
if (objread.Read())
{
if (objread["SystemFile"] != DBNull.Value)
data = (byte[])objread["SystemFile"];
}
}
}
}

return data;
}
}
}

مثالی در مورد روش استفاده از کلاس فوق :

using System.IO;

namespace FileStreamTest
{
class Program
{
static void Main(string[] args)
{
CFS.AddNewRecord(@"C:\filest05.PNG", "test1");

//آی دی رکورد ذخیره شده در دیتابیس برای مثال
byte[] data = CFS.GetDataFromDb("BB848D45-382C-4D95-BF4E-52C3509407D4");
if (data != null)
{
File.WriteAllBytes(@"C:\tst.PNG", data);
}
}
}
}
روش فوق با روش متداول افزودن یک فایل به دیتابیس اس کیوال سرور هیچ تفاوتی ندارد و این‌جا هم بدون مشکل کار می‌کند. اطلاعات نهایی به صورت فایل‌هایی بر روی سیستم که توسط اس کیوال سرور مدیریت خواهند شد و با جدول شما یکپارچه‌اند، ذخیره می‌شوند.

در روش دیگری که در اکثر مقالات مرتبط مورد استفاده است، از شیء SqlFileStream کمک گرفته شده و نحوه‌ی انجام آن نیز به صورت زیر می‌باشد.
در ابتدا دو رویه ذخیره شده زیر را ایجاد می‌کنیم:

CREATE PROCEDURE [AddFile](@Title NVARCHAR(255), @filepath VARCHAR(MAX) OUTPUT)
AS
BEGIN
SET NOCOUNT ON;

DECLARE @ID UNIQUEIDENTIFIER
SET @ID = NEWID()

INSERT INTO [tblFiles]
(
[FileId],
[title],
[SystemFile]
)
VALUES
(
@ID,
@Title,
CAST('' AS VARBINARY(MAX))
)

SELECT @filepath = SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
GO

CREATE PROCEDURE [GetFilePath](@Id VARCHAR(50))
AS
BEGIN
SET NOCOUNT ON;

SELECT SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
در رویه ذخیره شده AddFile ، ابتدا رکوردی بر اساس عنوان دلخواه ورودی با یک فایل خالی ایجاد می‌شود. سپس مسیر سیستمی این فایل را در آرگومان خروجی filepath قرار می‌دهیم. SystemFile.PathName از اس کیوال سرور 2008 جهت فیلدهای فایل استریم به اس کیوال سرور اضافه شده است. از این مسیر در برنامه خود جهت نوشتن بایت‌های فایل مورد نظر در آن توسط شیء SqlFileStream استفاده خواهیم کرد.
رویه ذخیره شده GetFilePath نیز تنها مسیر سیستمی فایل استریم ذخیره شده را بر می‌گرداند.
به این ترتیب کدهای برنامه به صورت زیر تغییر خواهند کرد:

using System.Data.SqlClient;
using System.Data;
using System.Data.SqlTypes;
using System.IO;

namespace FileStreamTest
{
class CFSqlFileStream
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده افزودن فایل
using (SqlCommand objSqlCmd = new SqlCommand(
"AddFile", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن وضعیت و مقدار پارامتر عنوان
SqlParameter objSqlParam1 = new SqlParameter("@Title", SqlDbType.NVarChar, 255);
objSqlParam1.Value = title;

//مشخص ساختن پارامتر خروجی رویه ذخیره شده
SqlParameter objSqlParamOutput = new SqlParameter("@filepath", SqlDbType.VarChar, -1);
objSqlParamOutput.Direction = ParameterDirection.Output;

//افزودن پارامترها به شیء کامند
objSqlCmd.Parameters.Add(objSqlParam1);
objSqlCmd.Parameters.Add(objSqlParamOutput);

//اجرای رویه ذخیره شده
objSqlCmd.ExecuteNonQuery();

//و سپس دریافت خروجی آن
string Path = objSqlCmd.Parameters["@filepath"].Value.ToString();

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای نوشتن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();
using (SqlFileStream objSqlFileStream =
new SqlFileStream(Path, objContext, FileAccess.Write))
{
objSqlFileStream.Write(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] buffer = null;

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده دریافت مسیر فایل
using (SqlCommand objSqlCmd =
new SqlCommand("GetFilePath", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن پارامتر ورودی رویه ذخیره شده و مقدار دهی آن
SqlParameter objSqlParam1 = new SqlParameter("@ID", SqlDbType.VarChar, 50);
objSqlParam1.Value = fileId;
objSqlCmd.Parameters.Add(objSqlParam1);

//اجرای رویه ذخیره شده و دریافت مسیر سیستمی فایل استریم
string path = string.Empty;
using (SqlDataReader sdr = objSqlCmd.ExecuteReader())
{
sdr.Read();
path = sdr[0].ToString();
}

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای خواندن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();

using (SqlFileStream objSqlFileStream =
new SqlFileStream(path, objContext, FileAccess.Read))
{
buffer = new byte[(int)objSqlFileStream.Length];
objSqlFileStream.Read(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}

return buffer;
}
}
}
در پایان برای تکمیل بحث می‌توان به مقاله‌ی مرجع زیر مراجعه کرد:
FILESTREAM Storage in SQL Server 2008

مطالب
بازسازی کد: تعریف متغیر توضیحی (Introduce explaining variable)
زمانیکه عبارت شرطی، یا محاسبات پیچیده‌ای در کد وجود دارد، می‌توان آن را به بخش‌های کوچکتری تقسیم و برای هر بخش، یک متغیر توضیحی ایجاد کرد؛ به طوری‌که نام متغیر، توضیح کافی ای در مورد آن بخش از عبارت باشد. 
در ادامه‌ی این بازسازی کد، معمولا می‌توان بازسازی کد جایگزینی متغیر موقتی با پرس و جو (Replace temp with query) را انجام داد. زیرا متغیرهای محلی صرفا در محدوده‌ی متد خود قابل دسترسی هستند و ممکن است توسعه‌ی یک متد بلند را به همراه بیاورند.
به طور مثال به تکه کد زیر توجه کنید:
if (platform.ToUpper().Contains("MAC") && browser.ToUpper().Contains("SAFARI")) 
{ 
        // do something 
}
این تکه کد با بررسی زیرساخت و مرورگر کاربر، کاری را انجام می‌دهد. اما با کمی دقت متوجه می‌شویم عبارت شرطی استفاده شده در آن کمی پیچیده است. می‌توان این کد را به صورت زیر بازسازی کرد. 
var isSafari = browser.ToUpper().Contains("SAFARI"); 
var isMac = platform.ToUpper().Contains("MAC"); 
if (isSafari && isMac) 
{ 
    // do something 
}
کد حاصل از بازسازی انجام شده، کدی خواناتر و قابل توسعه‌تر است (البته در این مثال با نمونه‌ای ساده سر و کار داریم و موارد واقعی‌تر معمولا پیچیدگی بیشتری دارند).  

مراحل انجام این بازسازی کد  

  1. متغیر محلی ای را ایجاد نمایید و بخشی از عبارت پیچیده را به آن مقداردهی کنید. 
  2. تمامی استفاده‌ها از آن بخش از عبارت را با مقدار متغیر جایگزین نمایید. 
  3. کد را کامپایل و تست نمایید. 
  4. برای بخش‌های دیگر عبارت پیچیده همین کارها را تکرار نمایید. 

مثال: با متدی که محاسبه قیمت نهایی یک کالا را انجام می‌دهد، شروع می‌کنیم.  
public decimal GetPrice() { 
    // price = base price - quantity * discount + shipping 
    return quantity * itemPrice - Math.Max(0, quantity - 500) * itemPrice * 0.05 
        + Math.Min(quantity * itemPrice * 0.1, 100); 
}
در اولین نگاه متوجه می‌شویم که می‌توان بخشی از محاسبات را که مربوط به قیمت پایه است، به صورت جداگانه در متغیری ذخیره نماییم:  
var basePrice = quantity * itemPrice;
با این تغییر، متد مورد نظر به شکل زیر خواهد شد:  
public decimal GetPrice() { 
    // price = base price - quantity * discount + shipping 
    var basePrice = quantity * itemPrice; 
    return basePrice - Math.Max(0, quantity - 500) * itemPrice * 0.05 
        + Math.Min(basePrice * 0.1, 100); 
}
سپس با کمی دقت می‌توانیم به فرمول مستقل محاسبه تخفیف برسیم و آن را در متغیری جداگانه ذخیره نماییم. با این کار متد مورد نظر به صورت زیر خواهد شد:  
public decimal GetPrice() { 
    // price = base price - quantity * discount + shipping 
    var basePrice = quantity * itemPrice; 
    var discount = Math.Max(0, quantity - 500) * itemPrice * 0.05; 
    return basePrice - discount 
        + Math.Min(basePrice * 0.1, 100); 
}
و پس از آن نیز فرمول مربوط به محاسبه مبلغ ارسال نیز کاملا واضح خواهد بود.  
public decimal GetPrice() { 
    // price = base price - quantity * discount + shipping 
    var basePrice = quantity * itemPrice; 
    var discount = Math.Max(0, quantity - 500) * itemPrice * 0.05; 
    var shipping = Math.Min(basePrice * 0.1, 100); 
    return basePrice – discount  + shipping; 
}

مشاهده می‌کنید که نمونه کد مطرح شده، به کدی منظم‌تر و خواناتر تبدیل شده است. یکی از مزایای جانبی چنین بازسازی کدی، نیاز کمتر به کامنت‌های توضیحی در مورد بدنه متد است. به طور مثال کامنت اولین خط از متد ذکر شده، در تکه کد نهایی بازسازی شده، کامنتی بی معنی است زیرا کد، به خودی خود کاری را که انجام می‌دهد، توضیح داده‌است. به این روش کد نویسی به طور کلی self documenting code گفته می‌شود. 
همان طور که در ابتدای مطلب نیز ذکر شد، در ادامه‌ی این بازسازی کد می‌توان بازسازی‌های دیگری مانند جایگزینی متغیر موقتی با پرس و جو را نیز استفاده نمود. این مورد به عنوان تمرین به خواننده واگذار می‌شود.  
مطالب
مسیریابی در Angular - قسمت هفتم - بهبودهای بصری
در این قسمت ویژگی‌های بصری را مانند مشخص سازی مسیر انتخاب شده، در منوی سایت و همچنین نمایش «لطفا منتظر بمانید» را در حین نمایش قسمت‌هایی که با تاخیر از سرور دریافت می‌شوند، پیاده سازی خواهیم کرد.


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

برای بهبود ظاهر برنامه نیاز است منوی سایت را به نحوی تغییر دهیم که مشخص کند، اکنون کاربر کدام گزینه را انتخاب کرده‌است. این مورد شامل سلسه مراتب مسیریابی‌ها نیز می‌شود؛ برای مثال فعالسازی حالت انتخاب شده‌ی منوی سایت، به همراه برگه‌ی انتخاب شده در یکی از Child Routes.
برای پیاده سازی این قابلیت، دایرکتیو ویژه‌ای به نام routerLinkActive تدارک دیده شده‌است. این دایرکتیو را می‌توان به یک anchor tag و یا المان والد آن انتساب داد. مقدار آن‌را نیز می‌توان به یکی از کلاس‌های CSS برنامه مانند کلاس active تعریف شده‌ی در بوت استرپ تنظیم کرد. هر زمانیکه این مسیریابی فعال شود، مسیریاب به صورت خودکار این کلاس را با درج آن، به المان مرتبط اضافه می‌کند و برعکس.
برای نمونه فایل src\app\product\product-edit\product-edit.component.html را گشوده و سپس تغییرات ذیل را اعمال کنید:
<div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
            </a>
</div>
در اینجا دایرکتیو‌های routerLinkActive، به هر کدام از لینک‌های تعریف شده اضافه گردیده‌اند. مقدار active در اینجا، به کلاس active بوت استرپ اشاره می‌کند. یا حتی می‌توان تعدادی کلاس جدا شده‌ی با کاما را نیز در اینجا ذکر کرد.

یک نکته: از آنجائیکه در اینجا مقدار active یک string است و نه یک خاصیت یا عبارت متغیر، به همین جهت نیازی نیست تا این دایرکتیو را به صورت [routerLinkActive] تعریف کنیم.


همانطور که مشاهده می‌کنید، همین دو تنظیم ساده سبب مشخص شدن برگه‌ی انتخابی شده‌اند.

منوی بالای سایت نیز چنین تنظیماتی را نیاز دارد. برای این منظور به فایل src\app\app.component.html که دربرگیرنده‌ی منوی سایت است مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
    <ul class="nav navbar-nav">
      <li routerLinkActive="active">
        <a [routerLink]="['/home']">Home</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products']">Product List</a>
      </li>
      <li routerLinkActive="active">
        <a [routerLink]="['/products', 0, 'edit']">Add Product</a>
       </li>      
    </ul>
اینبار routerLinkActive به المان‌های li اعمال شده‌است؛ چون این المان‌های لیست، شیوه نامه‌ی المان‌های anchor را بازنویسی می‌کنند و اگر routerLinkActive را به لینک‌ها اعمال می‌کردیم، تغییری مشاهده نمی‌شد.


همانطور که مشاهده می‌کنید، در این حالت انتخاب منوی نمایش لیست محصولات، سبب تزئین آن به حالت انتخاب شده نیز گردیده‌است.

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


اینبار هر دو گزینه با هم انتخاب شده‌اند. علت اینجا است که این دو مسیر دارای root URL segment یکسانی هستند؛ یا همان products/ در اینجا. به همین جهت routerLinkActive هر دو را به عنوان فعال انتخاب کرده‌است. برای مدیریت میدان دید آن می‌توان از دایرکتیو دیگری به نام routerLinkActiveOptions استفاده کرد:
      <li routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">
        <a [routerLink]="['/products']">Product List</a>
      </li>
routerLinkActiveOptions را تنها به ریشه‌ی مسیر products اعمال کرده‌ایم؛ چون این مسیر است که می‌تواند با تمام مسیرهای مشتق شده‌ی از آن نیز تطابق داشته باشد. تنظیم exact: true آن سبب خواهد شد تا تطابق با مسیرهای مشتق شده‌ی از آن ندید گرفته شوند.


اکنون کاربران بهتر می‌توانند درک کنند در کجای برنامه قرار دارند.


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

در ادامه می‌خواهیم اگر برگه‌ای دارای مشکلات اعتبارسنجی بود، آیکن خطایی را در کنار برچسب آن برگه نمایش دهیم. به این ترتیب مدیریت چندین برگه برای کاربران ساده‌تر خواهد شد و به سادگی می‌توانند برگه‌های مشکل دار را پیدا کنند.
در انتهای مطلب «مسیریابی در Angular - قسمت پنجم - تعریف Child Routes» متد isValid را تعریف کردیم. این متد مسیر یک tab را دریافت کرده و اگر اعتبارسنجی آن مشکلی نداشت، مقدار true را بر می‌گرداند. از این متد جهت نمایش آیکن خطای اعتبارسنجی برگه‌ها استفاده خواهیم کرد.
        <div class="wizard">
            <a [routerLink]="['info']" routerLinkActive="active">
                Basic Information
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('info')}"></span>
            </a>
            <a [routerLink]="['tags']" routerLinkActive="active">
                Search Tags
                <span [ngClass]="{'glyphicon glyphicon-exclamation-sign': !isValid('tags')}"></span>
            </a>
        </div>
در اینجا دو span را تعریف کرده‌ایم که با کمک دایرکتیو ngClass سبب نمایش آیکن exclamation-sign در صورت وجود یک خطای اعتبارسنجی می‌شوند. به عبارتی اگر برگه‌ای معتبر نباشد، سبب درج کلاس آن در span جاری می‌شود:



رخ‌دادهای مسیریابی

هر زمانیکه کاربری مسیرهای مختلف برنامه را پیمایش می‌کند، مسیریاب تعدادی رخ‌داد را نیز تولید خواهد کرد. از این رخ‌دادها جهت تحت نظر قرار دادن، عیب‌یابی و یا اجرای منطقی می‌توان استفاده کرد. این رخ‌دادها شامل موارد ذیل هستند:
- NavigationStart، با آغاز پیمایش یک مسیر رخ می‌دهد.
- RoutesRecognized، با تشخیص و تطابق یک مسیر، با یکی از المان‌های تعریف شده‌ی در تنظیمات مسیریابی رخ می‌دهد.
- NavigationEnd، با پایان پیمایش یک مسیر رخ می‌دهد.
- NavigationCancel، در صورت لغو پیمایش یک مسیریابی توسط محافظ‌های مسیرها و یا هدایت به یک جهت دیگر رخ می‌دهد.
- NavigationError، با شکست پیمایش یک مسیر رخ می‌دهد.

این رخ‌دادها با فعالسازی تنظیم enableTracing تنظیمات مسیریابی به true فعال می‌شوند. برای این منظور فایل src\app\app-routing.module.ts را گشوده و به نحو ذیل تغییر دهید:
@NgModule({
  imports: [RouterModule.forRoot(routes/*, { useHash: true }*/, { enableTracing: true })],
پس از این تغییر، اگر به developer tools مرورگر دقت کنید، یک چنین خروجی را می‌توان مشاهده کرد:


در اینجا ترتیب اجرای رخ‌دادهای متفاوت پیمایش مسیر نمایش لیست محصولات را مشاهده می‌کنید.
- Router به هر مسیر، یک id خود افزایش یابنده را به صورت خودکار نسبت می‌دهد. برای نمونه، این مسیر خاص، id:2 را یافته‌است. از این id می‌توان برای دسترسی به مجموعه‌ای از رخ‌دادها استفاده کرد.
- در این خروجی، url همان آدرس اصلی مسیر است و urlAfterRedirects به معنای مسیری است که پس از تنظیم redirect در تنظیمات مسیریابی (در صورت وجود) حاصل شده‌است.
- یکی از روش‌هایی که برای دیباگ مسیریابی‌ها می‌توان استفاده کرد، همین فعالسازی enableTracing است.


کار با رخ‌دادهای مسیریابی با کدنویسی

به رخ‌دادهایی که در کنسول developer tools مرورگر مشاهده کردید، با کدنویسی نیز می‌توان دسترسی یافت. برای مثال می‌توان یک تصویر چرخنده یا لطفا منتظر بمانید را در آغاز پیمایش یک مسیریابی نمایش داد و سپس در پایان پیمایش این مسیریابی، آن‌را مخفی کرد. این events نیز از نوع Observable بوده و برای کار با آن‌ها باید مشترکشان شد:
this.router.events.subscribe((routerEvent: Event) => {
    if (routerEvent instanceof NavigationStart) {
      //...
    }
});
شیء router به همراه خاصیت events است که با گوش فرادادن به رخ‌دادهای صادر شده‌ی توسط آن می‌توان دریافت چه نوع رخ‌دادی هم اکنون صادر شده‌است.

در مثال جاری این سری، در «مسیریابی در Angular - قسمت چهارم - پیش واکشی اطلاعات»، سبب شدیم تا کل اطلاعات مورد نیاز یک مسیر، پیش از نمایش آن از سرور دریافت شوند تا به این صورت ابتدا یک قاب خالی نمایش داده نشده و پس از مدتی تکمیل شود. هرچند تجربه‌ی کاربری این روش بهتر از روش قبلی است، اما هنوز هم کاربر تاخیری را در ابتدا حس خواهد کرد (به اندازه‌ی زمان delay تنظیم شده)، بدون اینکه راهنمایی به او ارائه شود. در این حالت بهتر است در ابتدای کار، یک تصویر چرخنده نمایش داده شود تا کاربر متوجه شود، نیاز است اندکی منتظر بماند.
در اینجا می‌خواهیم این تصویر چرخنده برای تمام مسیرهای برنامه فعال شود. به همین جهت گوش فرادادن به رخ‌دادها را در نقطه‌ی آغازین برنامه و یا همان src\app\app.component.ts انجام می‌دهیم:
import { Router, Event, NavigationStart, NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';

import { AuthService } from './user/auth.service';
import { Component } from '@angular/core';


@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  pageTitle: string = 'Routing Lab';

  loading: boolean = true;

  constructor(private authService: AuthService,
    private router: Router) {
    router.events.subscribe((routerEvent: Event) => {
      this.checkRouterEvent(routerEvent);
    });
  }

  checkRouterEvent(routerEvent: Event): void {
    if (routerEvent instanceof NavigationStart) {
      this.loading = true;
    }

    if (routerEvent instanceof NavigationEnd ||
      routerEvent instanceof NavigationCancel ||
      routerEvent instanceof NavigationError) {
      this.loading = false;
    }
  }

  logOut(): void {
    this.authService.logout();
    this.router.navigateByUrl('/welcome');
  }
}
کدهای کامل AppComponent را جهت گوش فرادادن به رخ‌دادهای شروع و یا خاتمه/لغو/شکست پیمایش یک مسیریابی، در اینجا مشاهده می‌کنید.
- ابتدا وابستگی‌های لازم آن import شده‌اند.
- سپس می‌خواهیم خاصیت عمومی loading را در شروع به پیمایش یک مسیر، به true تنظیم کنیم و اگر این پیمایش به هر نحوی خاتمه یافت، آن‌را false خواهیم کرد.

اکنون برای استفاده‌ی از این خاصیت عمومی و نمایش تصویر چرخنده، نیاز است قالب src\app\app.component.html را ویرایش کنیم:
<span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="loading"></span>
با افزودن span فوق به ابتدای فایل app.component.html به تغییرات خاصیت loading واکنش نشان خواهیم داد. کلاس‌های CSS ایی را که در اینجا اضافه شده‌اند، به فایل src\styles.css اضافه می‌کنیم:
/* Spinner */
.spinner {
  font-size:300%;
  position:absolute;
  top: 50%;
  left: 50%;
  z-index:10
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}


اکنون مسیرهایی که دارای route resolver هستند (مانند نمایش جزئیات/ویرایش یک محصول)، به همراه یک spinner نمایش داده خواهند شد و سایر مسیرها ابتدا نمایش داده خواهند شد و سپس اطلاعات آن‌ها از سرور دریافت می‌شود (مانند مسیر نمایش لیست محصولات که دارای route resolver نیست).
البته می‌توان این true/false کردن loading را به ابتدا و انتهای کار یک Observable، مانند حالت نمایش لیست محصولات نیز منتقل کرد. اما در این حالت باید span مرتبط را نیز به قالب همان کامپوننت انتقال داد و دیگر سراسری نخواهد بود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.