نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت هشتم - دریافت اطلاعات از سرور
من تغییرات RC 4 را اعمال کردم .الان مشکلی که دارم در متد ngOnInit متدی که از سرویس اطلاعات را دریافت می‌کند صدا زده شده(loadMenu)و در این متد متغیر menus مقداردهی شده اما برای خواندن از آن متغیر مقدار آن undefined  می‌باشد.البته متغیر در اجرای اول undefined است ولی با زدن دکمه F5 متغیر مقدار می‌گیرد. کش مرورگر هم پاک کردم اما مشکل حل نشد.کسی با این مشکل برخورد کرده؟
export class MenuComponent implements OnInit, AfterViewInit {
    menus: IMenu[];
  constructor(private _menuService: MenuService) {
  }
 ngOnInit(): void {
        this.loadMenu();
        alert(this.menus);// menus is undefined 
    }
مطالب
متغیرها در ES 6
در ES 6 تغییراتی جهت ساده سازی خواندن، نوشتن و همچنین بالا بردن امنیت متغیرها و پارامترها صورت گرفته‌اند، تا دیگر شاهد یک سری رفتارهای عجیب و غریب، در حین کار با متغیرهای جاوا اسکریپتی نباشیم.


واژه‌ی کلیدی let

تاکنون به کمک واژه‌ی کلیدی var امکان تعریف متغیرها در جاوا اسکریپت مهیا بودند. برای نمونه در مثال زیر، متغیر x داخل بدنه‌ی if با استفاده از var تعریف شده‌است:
var doWork = function(flag){
   if(flag){
     var x = 3;
   }
   return x;
};
در اینجا اگر متد doWork را با پارامتر true اجرا کنیم، خروجی 3  و اگر آن‌را با پارامتر false اجرا کنیم، خروجی undefined را دریافت خواهیم کرد:


زمانیکه از var استفاده می‌شود، برای یک متغیر دو نوع میدان دید را می‌توان متصور شد:
- اگر خارج از بدنه‌ی تابع تعریف شود، این متغیر عمومی خواهد بود.
- اگر داخل بدنه‌ی تابع تعریف شود، میدان دید آن محدود به همان بدنه‌ی تابع می‌شود. در این حالت چیزی به نام block scope بی‌مفهوم است. در متد doWork فوق، هرچند متغیر x داخل بدنه‌ی بلاک if تعریف شده‌است، اما این x در کل بدنه‌ی تابع در دسترس است و نه صرفا داخل بلاک if. این مورد تا پیش از ES 6 منشاء بسیاری از باگ‌ها بوده‌است.
بنابراین در اینجا چون x تعریف شده، میدان دیدی در سطح متد دارد، return x معتبر بوده و در حالت دریافت پارامتر true، مقدار 3 را بر می‌گرداند و در حالت false هم همچنان مقداری را دریافت خواهیم کرد و این مقدار undefined است (اما پیام خطای عدم دسترسی به x را دریافت نمی‌کنیم).
به این رفتار اصطلاحا hoisting می‌گویند. در این حالت موتور جاوا اسکریپت، تمام متغیرهای تعریف شده‌ی توسط var را به صورت ضمنی به ابتدای تعریف متد منتقل کرده و آن‌ها را در آن‌جا تعریف می‌کند. به همین جهت است که return x تعریف شده‌ی در انتهای متد، قابلیت دسترسی به x داخل بدنه‌ی if را دارد.

در ES 6 برای رفع این مشکل، واژه‌ی کلیدی جدیدی به نام let معرفی شده‌است و هدف آن مهیا کردن block scoping تعریف متغیرها است:
var doWork = function(flag){
    if(flag){
      let x = 3;
    }
    return x;
};
اینبار اگر متد doWork را با پارامتر true فراخوانی کنیم، به خروجی ذیل خواهیم رسید:


بله. همانطور که مشاهده می‌کنید، اینبار میدان دید x به if block تعریف شده‌ی در آن محدود گشته و دیگر خارج از آن مفهومی ندارد و تعریف نشده‌است. به همین جهت زمانیکه به return x می‌رسیم، پیام تعریف نشده بودن x را دریافت خواهیم کرد. برای اینکه قطعه کد فوق کار کند، نیاز است return x را به داخل بدنه‌ی قطعه‌ی if تعریف شده، انتقال داد.

این block scoping مهیا شده‌ی توسط let، با حلقه‌ی for نیز کار می‌کند:
var doWork = function(){
   for(let i = 0; i< 10; i++){
   }

   /* return i won't work */
  return 0;
};
در مثال فوق اگر return i را در انتهای متد قرار دهیم، با همان خطای Uncaught ReferenceError پیشین مواجه خواهیم شد؛ از این جهت که برخلاف var، متغیر تعریف شده‌ی با let، میدان دیدی در سطح قطعه و بلاک تعریف شده‌ی در آن دارد و در اینجا بلاک متغیر i همان حلقه‌ی for است.


یک نکته

مفهوم block scoping با تعریف {} معنا پیدا می‌کند. بنابراین می‌توانید یک قطعه‌ی دلخواه را با تعریف {} نیز مشخص کنید:

و یا در مثال ذیل چندین قطعه‌ی تو در تو را مشاهده می‌کنید:
let outer = 'I am so eccentric!'
{
  let inner = 'I play with neighbors in my block and the sewers'
  {
    let innermost = 'I only play with neighbors in my block'
  }
  // accessing innermost here would throw
}
// accessing inner here would throw
// accessing innermost here would throw
در اینجا میدان دید متغیرهای تعریف شده، محدود است به قطعه‌ی آن‌ها. به همین جهت است که نمی‌توان به متغیر innermost در خارج از بلاک آن دسترسی یافت.

نمونه‌ی دیگر آن تعریف یک متد داخل یک بلاک است:
{
  let _nested = 'secret'
  function nested () {
    return _nested
  }
}
console.log(nested())
اگر این قطعه کد را اجرا کنیم، به خطای ذیل خواهیم رسید:


در ES 6 نمی‌توان به متغیرهای تعریف شده‌ی توسط let داخل یک بلاک، در خارج از آن دسترسی یافت. اگر می‌خواهید سطح دسترسی به متد را افزایش دهید، نیاز است به شکل ذیل عمل کنید و متد را خارج از بدنه‌ی بلاک با سطح دسترسی بیشتری تعریف نمائید:
var nested;
{
  let _nested = 'secret'
  nested = function () {
    return _nested
  }
}
console.log(nested())
// <- 'secret'


واژه‌ی کلیدی const

در ES 6 برای ایجاد و مقدار دهی متغیرهای فقط خواندنی، واژه‌ی کلیدی const افزوده شده‌است. در اینجا const نیز مانند let دارای block scoping است.
doWork = function()
{
   const value = 10;
   value = 11;
   return value;
}
در این مثال ابتدا متغیر value به صورت یک ثابت تعریف شده‌است و سپس مقدار 11 به آن نسبت داده شده‌است. اگر آن‌را در کروم 47 اجرا کنید، از مقدار 11 صرفنظر شده و خروجی 10 را بازگشت می‌دهد. اما اگر آن‌را در فایرفاکس 43 اجرا کنید، خطای متناظر با ES 6 را بازگشت می‌دهد:


در ES 6، انتساب یک مقدار به یک const، پس از تعریف آن، منجر به بروز خطای syntax error خواهد شد. همچنین تعریف مجدد آن نیز چنین خطایی را سبب خواهد شد.

یک نکته
هر چند const سبب read only شدن یک متغیر می‌شود، اما آن‌را immutable نمی‌کند:
const items = { people: ['you', 'me'] }
items.people.push('test')
console.log(items)
با این خروجی:

همانطور که مشاهده می‌کنید، هنوز هم می‌توان به شیء تعریف شده، آیتمی را اضافه کرد (در اینجا test به آرایه‌ی people اضافه شده‌است).


آشنایی با مفهوم shadowing

همان مثال ابتدای بحث را در نظر بگیرید:
var doWork = function(flag){
   if(flag){
        let x = 10;
        var x = 3;
        return x;
   }
};
داخل بدنه‌ی if، متغیر x یکبار توسط let و بار دیگر توسط var تعریف شده‌است.  در این حالت خطای Uncaught SyntaxError: Identifier 'x' has already been declared را دریافت خواهیم کرد (اگر let اول را به var تغییر دهید، مشکلی نخواهد بود و برنامه کامپایل می‌شود). اما اگر let x را به پیش از متد انتقال دهیم، اینبار مثال کامپایل می‌شود و خروجی متد (doWork(true مساوی 3 خواهد بود:
let x = 10;
var doWork = function(flag){
    if(flag){
      var x = 3;
      return x;
   }
};
در این حالت x تعریف شده‌ی داخل بلاک توسط var (یا حتی let) مقدار x تعریف شده‌ی در بلاک بالاتر را مخفی می‌کند که به آن shadowing نیز می‌گویند. در این حالت اگر در خارج از متد doWork، به x دسترسی پیدا کنیم، مقدار آن همان 10 است.
مثال ذکر شده، با مثال ذیل که یک بلاک را توسط {} ایجاد کرده‌ایم، یکی است:
let x = 10;
{
   let x = 3;
   console.log(x);
}
console.log(x);


در اینجا نیز ابتدا مقدار 3 که مرتبط با بلاک داخلی است چاپ خواهد شد و سپس مقدار 10 که مرتبط است به بلاک خارجی‌تر.
مطالب
آشنایی با Feature Toggle - بخش اول
فرض کنید میخواهید برای بخش‌هایی از نرم افزاری که طراحی کرده‌اید ، امکانی را در نظر بگیرید که بتوانید زمانیکه نرم افزار در حال استفاده‌است، قابلیت‌هایی از آن‌را فعال یا غیرفعال نمایید؛ بدون اینکه نرم افزار از دسترس خارج شود. Feature Toggle که تحت عنوان Feature Flag هم شناخته می‌شود همین امکان را برای ما به ارمغان می‌آورد و ما را قادر می‌سازد تا قابلیت‌هایی را از نرم افزار، فعال یا غیرفعال کنیم، بدون اینکه نیاز باشد نرم افزار از دسترس مشتریان خارج شود و یا نیاز باشد نسخه‌ی جدیدی از نرم افزار  منتشر شود. برای مثال قابلیت ثبت نام کاربران را در بازه‌های خاصی غیرفعال کنیم و یا فرض کنید قابلیت جدیدی به نرم افزار اضافه کرده‌اید و میخواهید بعد از پابلیش، در یک بازه زمانی که نرم افزار شما بازدید کننده‌های کمتری دارد، آن‌را موقتا فعال کنید، نتیجه خروجی را ببینید و سپس آن را غیر فعال نمایید. در ادامه این مقاله سعی خواهیم کرد ابتدا با یک مثال ساده با این قابلیت آشنا شویم و سپس به معرفی یکی از کتابخانه‌های محبوب در این زمینه بپردازیم.
Feature Toggle چیزی بیشتر از یک دستور IF نیست، اگر شرط مورد نظر برقرار بود، کد را اجرا میکند، در غیر اینصورت از اجرای آن بخش صرف نظر میکند.
IF (currentYear<2023){
alert('Wear a mask!');
}
در قطعه کد فوق، سال جاری را چک کرده‌ایم و گفته‌ایم اگر سال جاری کمتر از سال 2023 بود، به بازدید کننده یک پیغام را نمایش دهیم. حال فرض کنید بیماری کرونا، پیش از سال 2023 از بین برود، ولی طبق این شرط همچنان پیغام به کاربران نمایش داده میشود. میتوانیم فعال و غیر فعال بودن نمایش این پیغام را یا از دیتابیس و یا از فایل appsetting.json  بخوانیم که در این حالت  به صورت زیر می‌باشد :
var showCoronaAlert=_cofiguration.GetValue<bool>("Features:showCoronaAlert"); // or read this from Database
If(showCoronaAlert){
alert(Wear a amask!);
}
در این روش بجای اینکه تاریخ را چک کنیم و بر اساس آن تصمیم بگیریم که آیا پیغامی نمایش داده شود یا نه، وضعیت نمایش آن را از فایل تنظیمات و یا دیتابیس خوانده‌ایم. در این حالت دیگر نیازی به تغییر و انتشار نسخه‌ی جدیدی از نرم افزار نیست و فقط کافی‌است مقدار مربوط به نمایش پیغام را در دیتابیس و یا فایل تنظیمات، به روزسانی نماییم.

 کتابخانه  Microsoft.FeatureManagement
کتابخانه  Microsoft.FeatureManagement  توسط تیم اژور پیاده سازی و نوشته شده‌است و برای خواندن اطلاعات، از همان IConfiguration استفاده میکند که ما را قادر می‌سازد تنظیمات را از منابع مختلفی بخوانیم  و همچنین  قابلیت‌های آن فراتر از تنظیم یک مقدار با true/false می‌باشد که در ادامه با بعضی از آنها آشنا خواهیم شد.
ابتدا نیاز هست این کتابخانه را به صورت زیر نصب نماییم :
Install-Package Microsoft.FeatureManagement

سپس نیاز هست در متد ConfigureService، سرویس مربوطه را اضافه نماییم :
using Microsoft.FeatureManagement;
public void ConfigureServices(IServiceCollection services)
{
    services.AddFeatureManagement();
}

این کتابخانه به صورت پیش فرض، اطلاعات feature‌ها را از بخشی (section) تحت عنوان FeatureManagement  از فایل appsetting.json می‌خواند. پس نیاز داریم این بخش را در appsetting.json تعریف نماییم  ( لیست تمامی قابلیت‌هایی را که قصد داریم به صورت داینامیک فعال/غیرفعال کنیم، در این بخش اضافه خواهیم کرد):
"FeatureManagement": {
   
}
اگر تمایل داشتید از اسم دیگری برای بخش تنظیمات، در فایل appsetting. json  استفاده نمایید، می‌توانید به صورت زیر این کار را انجام دهید :
public void ConfigureServices(IServiceCollection services)
{
 services.AddFeatureManagement(Configuration.GetSection("MyFeatureManagement"))
}
در این مقاله از همان اسم پیش فرض استفاده شده است.
افزودن یک قابلیت جدید
"FeatureManagement": {
   "MaskAlert":true
}

همان مثال بالا را  در بخش FeatureManagement  اضافه کرده‌ایم  و مقدار true را به معنی فعال بودن، برای آن در نظر گرفته‌ایم. این حالت، ساده‌ترین روش ثبت یک قابلیت با استفاده از این کتابخانه می‌باشد. برای بررسی وضعیت هر کدام از قابلیت‌ها باید اینترفیس  IFeatureManager   را به کلاس مربوطه تزریق نماییم و سپس بر اساس نام قابلیت، وضعیت آن را بررسی نماییم:
 public class HomeController : Controller
    {
        private readonly IFeatureManager _featureManager;

        public HomeController(IFeatureManager featureManager)
        {
            _featureManager = featureManager;
        }
        public async Task<IActionResult> Index()
        {
            if(await _featureManager.IsEnabledAsync("MaskAlert"))
            {
                // show messeage
            }

            return View();
        }
    }
اگر نیاز هست از اسم دیگری برای بخش (section)

فعال سازی بر اساس تاریخ (TimeWindowsFilter)
یکی از قابلیت‌های این کتابخانه، فعال سازی بر اساس بازه زمانی هست. اگر نیاز دارید یک قابلیت در یک بازه‌ی خاص فعال شود، میتوانید از این قابلیت استفاده کنید. برای فعال سازی این امکان، باید فیلتر TimeWindowFilter را که به صورت توکار به همراه کتابخانه وجود دارد، به صورت زیر در متد configureServices ثبت نماییم:
public void ConfigureServices(IServiceCollection services)
{ 
    services.AddFeatureManagement().AddFeatureFilter<TimeWindowFilter>();
}

و سپس یک Feature را در بخش FeatureManagement همانند زیر تعریف میکنیم که توسط آن مشخص کرده‌ایم این قابلیت در بازه‌ی زمانی بین دو تاریخ تعریف شده، فعال باشد :
 "FeatureManagement": {
    "EmergencyBanner": {
      "EnabledFor": [
        {
          "Name": "Microsoft.TimeWindow",
          "Parameters": {
            "Start": "01 Mar 2021 12:00:00 +00:00",
            "End": "01 Apr 2021 12:00:00 +00:00"
          }
        }
      ]
    }
  }
و نحوه‌ی بررسی فعال بودن آن، همانند روش قبل می‌باشد و فقط کافیست اسم Feature را به متد IsEnabledAsync بدهیم:
if(await _featureManager.IsEnabledAsync("EmergencyBanner")){
// show Emergency banner 
}

 پارامتر‌های Start و End میتوانند به صورت تکی هم استفاده شوند؛ به این معنا که میتوانید فقط پارامتر start را مقدار دهی کنید و در این حالت از تاریخ مورد نظر به بعد، Feature مورد نظر فعال می‌باشد و یا اگر فقط پارامتر End مقدار دهی شود، Feature مورد نظر فقط تا تاریخ تعیین شده فعال هست و بعد از آن برای همیشه غیرفعال می‌شود.
در زیر، نمونه‌ای از این حالت تنظیم شده‌است :
"FeatureManagement": {
    "EmergencyBanner": {
      "EnabledFor": [
        {
          "Name": "Microsoft.TimeWindow",
          "Parameters": {
            "End": "01 Apr 2021 12:00:00 +00:00"
          }
        }
      ]
    }
  }

فیلتر‌های سفارشی
از دیگر مزایای این کتابخانه این هست که محدود به فیلترهای توکار خود آن نیستیم و امکان توسعه و نوشتن فیلتر‌های سفارشی را به ما میدهد. برای مثال اگر یک قابلیت را در نرم افزار پیاده سازی کرده‌ایم که میخواهیم فقط بر روی مرورگر‌های خاصی در دسترس باشد، میتوانیم به صورت زیر این کار را انجام دهیم:
ابتدا در appsetting.json قابلیت (Feature) مورد نظر را به صورت زیر تعریف می‌کنیم :
"FeatureManagement": {
    "ChatV2": {
      "EnabledFor": [
        {
          "Name": "BrowserFilter",
          "Parameters": {
            "AllowedBrowsers": [ "Chrome" ]
          }
        }
      ]
    }
  }
سپس فیلتر سفارشی را به صورت زیر پیاده سازی میکنیم :
[FilterAlias("BrowserFilter")]
public class BrowserFilter:IFeatureFilter
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public BrowserFilter(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext context)
        {
            var userAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"].ToString();
            var settings = context.Parameters.Get<BrowserFilterSettings>();
            return Task.FromResult(settings.AllowedBrowsers.Any(userAgent.Contains));
        }
    }

کلاس BrowserFilter :
  public class BrowserFilterSettings
    {
        public string[] AllowedBrowsers { get; set; }
    }
بعد از پیاده سازی فیلتر فوق نیاز هست فیلتر سفارشی را که در بالا نوشتیم، در متد ConfigureServices ثبت نماییم. با توجه به اینکه برای تشخیص نوع مروگر کاربر نیاز هست  هدر درخواست را بررسی کنیم، پس نیاز هست IHttpContextAccessor را هم ثبت نماییم:
public void ConfigureServices(IServiceCollection services)
        {
            services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddFeatureManagement()
                .AddFeatureFilter<BrowserFilter>();
        }
و برای بررسی فعال بودن قابلیت مورد نظر فقط کافیست مانند قبل، اسم قابلیت مورد نظر را به صورت زیر بررسی کنیم :
if(await _featureManager.IsEnabledAsync("ChatV2")){
// do something 
}

* از دیگر قابلیت‌های این کتابخانه، فعال و غیر فعال کردن کنترلر و اکشن متدها بر اساس وضعیت Feature‌ها می‌باشد که در بخش دوم این مقاله به توضیح این موارد خواهیم پرداخت.
مطالب
ASP.NET MVC #19

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

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

using System.Web.Mvc;

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

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


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

using System;
using System.Web.Mvc;

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

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

@{
ViewBag.Title = "Index";
}

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

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


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

using System;
using System.Web.Mvc;

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


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

@{
ViewBag.Title = "Index";
}

<h2>@ViewBag.Msg</h2>

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

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

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

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

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



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

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

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

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

using System;
using System.Web.Mvc;

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

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


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

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


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

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

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


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

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

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

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

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


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

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

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

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

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

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

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


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




نظرات مطالب
ساختار پروژه های Angular
ضمن تشکر فراوان از جناب آقای پاکدل عزیز، در این مقاله به خوبی درباره lazy loading در angularjs بحث شده. نکته مهم اینکه حتما پروژه‌ی قابل اجرایی که در انتهای مقاله لینک شده را ملاحظه کنید. نکاتی در این پروژه هست از جمله اینکه برای دسترسی به providerها برای lazy loading آنها به این ترتیب به app افزوده شده اند:
app.config([
        '$stateProvider',
        '$urlRouterProvider',
        '$locationProvider',
        '$controllerProvider',
        '$compileProvider',
        '$filterProvider',
        '$provide',


        function ($stateProvider, $urlRouterProvider, $locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
            //برای رجیستر کردن غیر همروند اجزای انگیولاری در آینده
            app.lazy =
            {
                controller: $controllerProvider.register,
                directive:  $compileProvider.directive,
                filter:     $filterProvider.register,
                factory:    $provide.factory,
                service:    $provide.service
            };
.
.
.
])
(البته این کد از پروژه خودمان است و بعضی وابستگی‌های دیگر هم تزریق شده‌اند).
استفاده از app.lazy باعث سهولت بیشتر در استفاده و خواناتر شدن کد می‌شود. در ادامه به این ترتیب می‌توانید از app.lazy استفاده کنید:
angular.module('app').lazy.controller('myController',
        ['$scope',  function($scope){
...
}]);
به این ترتیب کد نوشته شده به دلیل نام گذاری ارجاع controllerProvider  با  controller  به حالت عادی شبیه است، و از طرفی lazy پیش از آن به فهم ماجرا کمک خواهد کرد.
این نقطه شروع یکی از پروژه‌های ماست که به عنوان نمونه بد نیست ملاحظه کنید:
<script type="text/javascript">
// --- Scriptjs ---
!function (a, b, c) { function t(a, c) { var e = b.createElement("script"), f = j; e.onload = e.onerror = e[o] = function () { e[m] && !/^c|loade/.test(e[m]) || f || (e.onload = e[o] = null, f = 1, c()) }, e.async = 1, e.src = a, d.insertBefore(e, d.firstChild) } function q(a, b) { p(a, function (a) { return !b(a) }) } var d = b.getElementsByTagName("head")[0], e = {}, f = {}, g = {}, h = {}, i = "string", j = !1, k = "push", l = "DOMContentLoaded", m = "readyState", n = "addEventListener", o = "onreadystatechange", p = function (a, b) { for (var c = 0, d = a.length; c < d; ++c) if (!b(a[c])) return j; return 1 }; !b[m] && b[n] && (b[n](l, function r() { b.removeEventListener(l, r, j), b[m] = "complete" }, j), b[m] = "loading"); var s = function (a, b, d) { function o() { if (!--m) { e[l] = 1, j && j(); for (var a in g) p(a.split("|"), n) && !q(g[a], n) && (g[a] = []) } } function n(a) { return a.call ? a() : e[a] } a = a[k] ? a : [a]; var i = b && b.call, j = i ? b : d, l = i ? a.join("") : b, m = a.length; c(function () { q(a, function (a) { h[a] ? (l && (f[l] = 1), o()) : (h[a] = 1, l && (f[l] = 1), t(s.path ? s.path + a + ".js" : a, o)) }) }, 0); return s }; s.get = t, s.ready = function (a, b, c) { a = a[k] ? a : [a]; var d = []; !q(a, function (a) { e[a] || d[k](a) }) && p(a, function (a) { return e[a] }) ? b() : !function (a) { g[a] = g[a] || [], g[a][k](b), c && c(d) }(a.join("|")); return s }; var u = a.$script; s.noConflict = function () { a.$script = u; return this }, typeof module != "undefined" && module.exports ? module.exports = s : a.$script = s }(this, document, setTimeout)

$script(['/Scripts/Lib/jquery/jquery-1.10.2.min.js'], function () {
    $script(['/Scripts/Lib/angular/angular.js'], function () {
        $script(['/Scripts/Lib/angular/angular-ui-router.min.js',
                    '/Scripts/Lib/angular/angular-resource.min.js',
                    '/Scripts/Lib/angular/angular-cache.min.js',
                    '/Scripts/Lib/angular/angular-sanitize.min.js',
                    '/Scripts/Lib/angular/angular-animate.min.js',
                    '/Scripts/Lib/angular/angular-cookie.min.js',
                    '/APP/Common/directives.js'
                ], function () {
                    $script('/app/app.js', function () {
                        angular.bootstrap(document, ['app']);
                    });
                });
            })
        });
</script>

این تگ script در صفحه شروع پروژه آمده است.
کد minify شده scriptjs در ابتدا قرار دارد، پس از آن فایل‌های js مورد نیاز با رعایت وابستگی‌های احتمالی به ترتیب بارگذاری شده‌اند.
این قسمت resolve یکی از بخش‌های مسیریابی است: 
resolve: {
                        fileDeps: ['$q', '$rootScope', function ($q, $rootScope) {
                            var deferred = $q.defer();
                            var deps = ['/app/HotStories/dataContextService.js',
                                        '/app/HotStories/hotStController.js'];
                            $script(deps, function () {
                                $rootScope.$apply(function () {
                                    deferred.resolve();
                                });
                            });

                            return deferred.promise;
                        }]
                    }
این نحوه تعریف سرویسی که فایل آن در وابستگی‌ها آمده و قرار است lazy load شود:
angular.module('app').lazy.service('dataContextService',
        ['$rootScope', '$resource', '$angularCacheFactory', '$q', function($rootScope, $resource, $cacheFactory, $q){
...
}]);
و این هم نحوه تعریف کنترلری که فایل آن در وابستگی‌ها آمده و قرار است lazy load شود: 
angular.module('app').lazy.controller('hotStController',
        ['$scope', 'ipCookie', 'dataContextService', function($scope, ipCookie, dataContextService){
...
}]);


مطالب
پشتیبانی آنلاین سایت با SignalR ،ASP.NET MVC و AngularJS
  پشتیبانی آنلاین سایت، روشی مناسب برای افزایش سطح تماس مشتریان با فروشندگان، برای جلوگیری از اتلاف وقت در برقراری تماس میباشد.
قصد داریم در این بخش پشتیبانی آنلاین سایت را با استفاده از AngularJs /Asp.Net Mvc / Signalr تهیه کنیم.
امکانات این برنامه:
* امکان مکالمه متنی به همراه ارسال شکلک
* امکان انتقال مکالمه
* مشاهده آرشیو گفتگوها
* امکان ارسال فایل (بزودی)
* امکان ذخیره گفتگو و ارسال گفتگو به ایمیل  (بزودی)
* امکان ارسال تیکت در صورت آفلاین بودن کارشناسان (بزودی) 
* رعایت مسائل امنیتی(بزودی)

مراحل نحوه اجرای برنامه:
1-  باز کردن دو tab، یکی برای کارشناس یکی  برای مشتری .
2-  تعدادی کارشناس تعریف شده است که با کلیک بر روی هر کدام وارد پنل کارشناس خواهیم شد.
3- شروع مکالمه توسط مشتری با کلیک بر روی chatbox پایین صفحه (سمت راست پایین).
4- شروع کردن مکالمه توسط کارشناس. 
5- ادامه،خاتمه یا انتقال مکالمه توسط کارشناس.

نصب کتابخانه‌های زیر:
//client
Install-Package angularjs 
Install-Package angular-strap 
Install-Package Microsoft.AspNet.SignalR.JS 
install-package AngularJs.SignalR.Hub 
Install-Package jQuery.TimeAgo
Install-Package FontAwesome
Install-Package toastr
Install-Package Twitter.Bootstrap.RTL 
bower install angular-smilies  

//server
Install-Package Newtonsoft.Json
Install-Package Microsoft.AspNet.SignalR 
Install-Package EntityFramework

گام‌های برنامه:
1-ایجاد جداول 
جدول Message: هر پیام دارای فرستنده و گیرنده‌ای، به همراه زمان ارسال میباشد.
جدول Session: شامل لیستی از پیام‌ها به همراه ارجاعی به خود (استفاده هنگام انتقال مکالمه )
 public partial class Message
    {
        public int Id { get; set; }
        public string Sender { get; set; }
        public string Receiver { get; set; }
        public string Body { get; set; }
        public DateTimeOffset? CreationTime { get; set; }
        public int? SessionId { get; set; }
        public virtual Session Session { get; set; }
    }
    public partial class Session
    {
        public Session()
        {
           Messages = new List<Message>();
           Sessions = new List<Session>();
        }
        public int Id { get; set; }
        public string AgentName { get; set; }
        public string CustomerName { get; set; }
        public DateTime CreatedDateTime { get; set; }
        public int? ParentId { get; set; }
        public virtual Session Parent { get; set; }
        public virtual ICollection<Message> Messages { get; set; }
        public virtual ICollection<Session> Sessions { get; set; }
    }

2- ایجاد ویو مدلهای زیر
    public class UserInformation
    {
        public string ConnectionId { get; set; }
        public bool IsOnline { get; set; }
        public string UserName { get; set; }
    }
    public class ChatSessionVm
    {
        public string Key { get; set; }
        public List<string> Value { get; set; }
    }
    public class AgentViewModel
    {
        public int Id { get; set; }
        public string CustomerName { get; set; }
        public int Lenght { get; set; }
        public DateTimeOffset? Date { get; set; }
    }

3- ایجاد Hub در سرور
 [HubName("chatHub")]
    public class ChatHub : Microsoft.AspNet.SignalR.Hub
    {
    }

4- فراخوانی chathub توسط کلاینت: برای آشنایی با سرویس  hub کلیک نمایید.

listeners متدهای سمت کلاینت
methods آرایه ای از متدهای سمت سرور

 $scope.myHub = new hub("chatHub", {
  listeners: {},
  methods: []
})
در صورت موفقیت آمیز بودن اتصال به هاب، متد init سمت سرور فراخوانی میشود و وضعیت آنلاین بودن کارشناسان برای کلاینت مشخص میشود.
 $scope.myHub.promise.done(function () {
     $scope.myHub.init();
     $scope.myHub.promise.done(function () { });
  });
 public void Init()
        {
            _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
            _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
            Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
        }

5-وضعیت کارشناسان :
در صورت آنلاین بودن کارشناسان: ارسال اولین پیام و تقاضای شروع مکالمه
در صورت آفلاین بودن کارشناسان: ارسال تیکت(بزودی)
اگر برای اولین بار  پیامی را ارسال میکنید، برای شما session ایی ایجاد نشده است. در اینصورت مکان تقاضای مشتری از سایت http://ipinfo.io دریافت شده و به سرور ارسال می‌گردد و  متد logvist سرور، تقاضای شروع مکالمه مشتری را به اطلاع  تمام کارشناسان میرساند و وضعیت chatbox را تغییر میدهد.
اگر session برای مشتری تعریف شده باشد، مکالمه مشتری با کارشناس مربوطه انجام میگردد.
 $scope.requestChat = function (msg) {
                if (!defaultCustomerUserName) {
                    //گرفتن کاربر لاگین شده
                    //ما از آرایه تصادفی استفاده میکنیم
                    var nameDefaultArray = [
                        'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                    ];
                    defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                }
                var userName = defaultCustomerUserName;
                if (!$scope.chatId) {
                    $scope.chatId = sessionStorage.getItem(chatKey);
                    $http.get("http://ipinfo.io")
                      .success(function (response) {
                          $scope.myHub.logVisit(response.city, response.country, msg, userName);
                      }).error(function (e, status, headers, config) {
                          $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                      });
                    $scope.myHub.requestChat(msg);
                    $scope.chatTitle = $scope.options.waitingForOperator;
                    $scope.pendingRequestChat = true;
                } else {
                    $scope.myHub.clientSendMessage(msg, userName);
                };
                $scope.message = "";
            };

6-مشاهده تقاضای مکالمه کاربران  توسط کارشناسان
کارشناسان در صورت تمایل، شروع به مکالمه با کاربر مینمایند و مکالمه آغاز میگردد.با شروع مکالمه توسط کارشناس، متد acceptRequestChat  سرور فراخوانی میشود.
 پیام‌های مناسب برای کارشناس مربوطه، برای مشتری و تمام کارشناسان (به تمام کارشناسان اطلاع داده می‌شود که مشتری با چه کارشناسی در حال مکالمه میباشد) ارسال میگردد و مقادیر مربوطه در دیتابیس ذخیره میگردد.
public void AcceptRequestChat(string customerConnectionId, string body, string userName)
        {
            var agent = FindAgent(Context.ConnectionId);
            var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
            if (session == null)
            {
                _chatSessions.Add(new ChatSessionVm
                {
                    Key = agent.Key,
                    Value = new List<string> { customerConnectionId }
                });
            }
            else
            {
                session.Value.Add(customerConnectionId);
            }
            Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
            Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
            foreach (var item in _agents.Where(item => item.Value.IsOnline))
            {
                Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
            }
       var session = _db.Sessions.Add(new Session
            {
                AgentName = agent.Key,
                CustomerName = userName,
                CreatedDateTime = DateTime.Now
            });
            _db.SaveChanges();

            var message = new Message
            {
                CreationTime = DateTime.Now,
                Sender = agent.Key,
                Receiver = userName,
                body=body,
                Session = session
            };
            _db.Messages.Add(message);
            _db.SaveChanges();
        }
7-خاتمه مکالمه توسط کارشناس یا مشتری امکان پذیر میباشد:
متد closeChat  سرور فراخوانی میگردد. پیام مناسبی به مشتری و تمام کارشناسان ارسال میگردد.
public void CloseChat(string id)
        {
            var findAgent = FindAgent(Context.ConnectionId);
            var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
            if (session == null) return;
            Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");

            foreach (var agent in _agents)
            {
                Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
            }
            _chatSessions.Remove(session);
        }

8-انتقال مکالمه مشتری به کارشناسی دیگر
مکالمه از کارشناس فعلی گرفته شده و به کارشناس جدید داده می‌شود؛ به همراه ارسال پیام‌های مناسب به طرف‌های مربوطه
   public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
        {
            #region remove session of current agent
            var currentAgent = FindAgent(Context.ConnectionId);
            var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
            if (currentSession != null)
            {
                _chatSessions.Remove(currentSession);
            }
            #endregion

            #region add  session to new agent
            var newAgent = FindAgent(newAgentId);
            var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
            if (newSession == null)
            {
                _chatSessions.Add(new ChatSessionVm
                {
                    Key = newAgent.Key,
                    Value = new List<string> { cumtomerId }
                });
            }
            else
            {
                newSession.Value.Add(cumtomerId);
            }
            #endregion

            Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
            Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                "لطفا مکالمه را ادامه دهید.با تشکر");

            Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);

            var session = _db.Sessions.FirstOrDefault
                (item => item.AgentName.Equals(currentAgent.Value.UserName)
                 && item.CustomerName.Equals(customerName));
            if (session != null)
            {
                var sessionId = session.Id;
                var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                Clients.Client(newAgentId).visitorSwitchConversation
                    (Context.ConnectionId, customerName, result, clientSessionId);
            }
            foreach (var item in _agents.Where(item => item.Value.IsOnline))
            {
                Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
            }
            _db.Sessions.Add(new Session
            {
                AgentName = newAgent.Key,
                CustomerName = customerName,
                CreatedDateTime = DateTime.Now,
                Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)
                      && item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
            });
            _db.SaveChanges();
        }
از آنجاییکه اسم متدها کاملا گویا میباشد، به نظر نیازی به توضیح بیشتری ندارند.
فایل کامل  app.js 
var app = angular.module("app", ["SignalR", 'ngRoute', 'ngAnimate', 'ngSanitize', 'mgcrea.ngStrap', 'angular-smilies']); 

app.config(["$routeProvider", "$provide", "$httpProvider", "$locationProvider",
        function ($routeProvider, $provide, $httpProvider, $locationProvider) {
            $routeProvider.
               when('/', { templateUrl: 'app/views/home.html', controller: "HomeCtrl" }).
               when('/agent', { templateUrl: 'app/views/agent.html', controller: "ChatCtrl" })
                .otherwise({
                    redirectTo: "/"
                });;
        }]);
app.controller("HomeCtrl", ["$scope", function ($scope) {
    $scope.title = "home";
}])
app.controller("ChatCtrl", ["$scope", "Hub", "$location", "$http", "$rootScope",
    function ($scope, hub, $location, $http, $rootScope) {
        if (!$scope.myHub) {
            var chatKey = "angular-signalr";
            var defaultCustomerUserName = null;
            function getid(id) {
                var find = false;
                var position = null;
                angular.forEach($scope.chatConversation, function (index, i) {
                    if (index.id === id && !find) {
                        find = true;
                        position = i;
                        return;
                    }
                });
                return position;
            }
            function apply() {
                $scope.$apply();
            }
            $scope.boxheader = function () {
                var height = 0;
                $("#chat-box").slideToggle('slow', function () {
                    if ($("#chat-box-header").css("bottom") === "0px") {
                        height = $("#chat-box").height() + 20;
                    } else {
                        height = 0;
                    }
                    $("#chat-box-header").css("bottom", height);
                });
            };
            var init = function () {
                $scope.agent = {
                    id: "", name: "", isOnline: false
                };
                $rootScope.msg = "";
                $scope.alarmStatus = false;
                $scope.options = {
                    offlineTitle: "آفلاین",
                    onlineTitle: "آنلاین",
                    waitingForOperator: "لطفا منتظر بمانید تا به اپراتور وصل شوید",
                    emailSent: "ایمیل ارسال گردید",
                    emailFailed: "متاسفانه ایمیل ارسال نگردید",
                    logOut: "خروج",
                    setting: "تنظیمات",
                    conversion: "آرشیو",
                    edit: "ویرایش",
                    alarm: "قطع/وصل کردن صدا",
                    complete: "تکمیل",
                    pending: "منتظر ماندن",
                    reject: "عدم پذیرش",
                    lock: "آنلاین شدن",
                    unlock: "آفلاین شدن",
                    alarmOn: "روشن",
                    alarmOff: "خاموش",
                    upload: "آپلود"
                };
                $scope.chatConversation = [];
                $scope.chatSessions = [];
                $scope.customerVisit = [];

                $scope.agentClientMsgs = [];
                $scope.clientAgentMsg = [];
            }();
//تعریف هاب به همراه متدهای آن
            $scope.myHub = new hub("chatHub", {
                listeners: {
                    "clientChat": function (id, agentName) {
                        $scope.clientAgentMsg.push({ name: agentName, msg: "با سلام در خدمت میباشم" });
                        $scope.chatTitle = "کارشناس: " + agentName;
                        $scope.pendingRequestChat = false;
                        sessionStorage.setItem(chatKey, id);
                    }, "agentChat": function (id, firstComment, customerName) {
                        var date = new Date();
                        var position = getid(id);
                        if (position > 0) {
                            $scope.chatSessions[position].length = $scope.chatConversation[position].length + 1;
                            $scope.chatSessions[position].date = date.toISOString();
                            return;
                        }
                        else {
                            $scope.chatConversation.push({
                                id: id,
                                sessions: [{
                                    name: customerName,
                                    msg: firstComment,
                                    date: date
                                }],
                                agentName: $scope.agent.name,
                                customerName: customerName,
                                dateStartChat: date.getHours() + ":" + date.getMinutes(),
                            });
                            $scope.chatSessions.push({
                                id: id,
                                length: 1,
                                userName: customerName,
                                date: date.toISOString()
                            });
                        }
                        sessionStorage.setItem(chatKey, id);
                        apply();
                    }, 
//برروز رسانی لیست برای کارشناسان
"refreshChatWith": function (agentName, customerConnectionId) {
                        angular.forEach($scope.customerVisit, function (index, i) {
                            if (index.connectionId === customerConnectionId) {
                                $scope.customerVisit[i].chatWith = agentName;
                            }
                        });
                        apply();
                    },
//برروز رسانی لیست برای کارشناسان
 "refreshLeaveChat": function (agentName, customerConnectionId) {
                        angular.forEach($scope.customerVisit, function (index, i) {
                            if (index.connectionId === customerConnectionId) {
                                $scope.customerVisit[i].chatWith =agentName + "---" + "  به مکالمه خاتمه داده است ";
                            }
                        });
                        apply();
                    }
//وضعیت آنلاین بودن کارشناسان
                    , "onlineStatus": function (state) {
                        if (state) {
                            $scope.chatTitle = $scope.options.onlineTitle;
                            $scope.hasOnline = true;
                            $scope.hasOffline = false;
                        } else {
                            $scope.chatTitle = $scope.options.offlineTitle;
                            $scope.hasOffline = true;
                            $scope.hasOnline = false;
                        }
                        $scope.$apply()
                    }, "loginResult": function (status, id, name) {
                        if (status) {
                            $scope.agent.id = id;
                            $scope.agent.name = name;
                            $scope.agent.isOnline = true;
                            $scope.userIsLogin = $scope.agent;
                            $scope.$apply(function () {
                                $location.path("/agent");
                            });
                        } else {
                            $scope.agent = null;
                            toastr.error("کارشناسی با این مشخصات وجود ندارد");
                            return;
                        }
                    }, "newVisit": function (userName, city, country, chatWith, connectionId, firstComment) {
                        var exist = false;
                        angular.forEach($scope.customerVisit, function (index) {
                            if (index.connectionId === connectionId) {
                                exist = true;
                                return;
                            }
                        });
                        if (!exist) {
                            var date = new Date();
                            $scope.customerVisit.unshift({
                                userName: userName,
                                date: date,
                                city: city,
                                country: country,
                                chatWith: chatWith,
                                connectionId: connectionId,
                                firstComment: firstComment
                            });
                            if ($scope.alarmStatus) {
                                var snd = new Audio("/App/assets/sounds/Sedna.ogg");
                                snd.play();
                            }
                            toastr.success("تقاضای جدید دریافت گردید");
                            apply();
                        }
                    }, "addMessage": function (id, from, value) {
                        if ($scope.alarmStatus) {
                            var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                            snd.play();
                        }
                        $scope.agentUserMsgs = [];
                        var date = new Date();
                        var position = getid(id);
                        if ($scope.chatConversation.length > 0 && position != null) {
                            $scope.chatConversation[position].sessions.push({ name: from, msg: value, date: date });
                        }
                        var item = $scope.chatConversation[position];
                        if (item) {
                            angular.forEach(item.sessions, function (index) {
                                $scope.agentUserMsgs.push({ name: index.name, msg: index.msg, date: date });
                            });
                            $scope.chatSessions[position].length = $scope.chatSessions[position].length + 1;
                        }
                        apply();
                    }, "clientAddMessage": function (id, from) {
                        if ($scope.alarmStatus) {
                            var snd = new Audio("/App/assets/sounds/newmsg.mp3");
                            snd.play();
                        }
                        $scope.clientAgentMsg.push({ name: id, msg: from });
                        apply();
                    }, "visitorSwitchConversation": function (id, customerName, sessions, sessionId) {
                        sessions = JSON.parse(sessions);
                        var date = new Date();
                        var sessionList = [];
                        angular.forEach(sessions, function (index) {
                            sessionList.push({
                                name: index.sender,
                                msg: index.body,
                                date: index.creationTime
                            });
                        });
                        $scope.chatConversation.push({
                            id: sessionId,
                            sessions: sessionList,
                            customerName: customerName,
                            dateStartChat: date.getHours() + ":" + date.getMinutes(),
                            agentName: $scope.agent.name
                        });
                        $scope.chatSessions.push({
                            id: sessionId,
                            length: sessions.length,
                            date: date
                        });
                    }, "receiveTicket": function (items) {
                        angular.forEach(JSON.parse(items), function (index) {
                            $scope.ticketList = [];
                            $scope.ticketList.push(index);
                        });
                    }, 
//آرشیو گفته گوهای کارشناس
"receiveHistory": function (items) {
                        $scope.agentHistory = [];
                        angular.forEach(JSON.parse(items), function (index) {
                            $scope.agentHistory.push(index);
                        });
                        apply();
                    }, 
//جزییات آرشیو گفتگوها
"detailsHistory": function (items) {
                        $scope.historyMsg = [];
                        angular.forEach(JSON.parse(items), function (index) {
                            $scope.historyMsg.push({ name: index.sender, msg: index.body, date: index.creationTime });
                        });
                        $("#detailsAgentHistory").modal();
                        apply();
                    }, 
//لیست کارشناسان آنلاین
"agentList": function (items) {
                        $scope.agentList = [];
                        angular.forEach(items, function (index) {
                            if ($scope.agent.name != index.Key) {
                                $scope.agentList.push({ name: index.Key, id: index.Value.ConnectionId });
                            }
                        });
                        $("#agentList").modal();
                        apply();
                    }
                },
                methods: ["agentConnect", "sendTicket", "requestChat", "clientSendMessage", "closeChat", "init", "logVisit",
                    "agentChangeStatus", "engageVisitor", "agentSendMessage", "transfer", "leaveChat", "acceptRequestChat",
                    "leaveChat", "detailsSessoinMessage", "showAgentList", "getAgentHistoryChat"
                ], errorHandler: function (error) {
                    console.error(error);
                }
            });
            $scope.myHub.promise.done(function () {
                $scope.myHub.init();
                $scope.myHub.promise.done(function () { });
            });

            $scope.LeaveChat = function () {
                $scope.myHub.LeaveChat();
            };
            $scope.loginAgent = function (userName) {
                // username :security user username from agent role
                if (userName == "hossein" || userName == "ali") {
                    $scope.myHub.promise.done(function () {
                        $scope.myHub.agentConnect(userName).then(function (result) {
                            $scope.agent.name = userName;
                            $scope.agent.isOnline = true;
                        });
                    });
                }
            };
            $scope.requestChat = function (msg) {
                if (!defaultCustomerUserName) {
                    //گرفتن کاربر لاگین شده
                    //ما از آرایه تصادفی استفاده میکنیم
                    var nameDefaultArray = [
                        'حسین', 'حسن', 'علی', 'عباس', 'زهرا', 'سمیه'
                    ];
                    defaultCustomerUserName=nameDefaultArray[Math.floor(Math.random() * nameDefaultArray.length)];
                }
                var userName = defaultCustomerUserName;
                if (!$scope.chatId) {
                    $scope.chatId = sessionStorage.getItem(chatKey);
                    $http.get("http://ipinfo.io")
                      .success(function (response) {
                          $scope.myHub.logVisit(response.city, response.country, msg, userName);
                      }).error(function (e, status, headers, config) {
                          $scope.myHub.logVisit("Tehran", "Ir", msg, userName)
                      });
                    $scope.myHub.requestChat(msg);
                    $scope.chatTitle = $scope.options.waitingForOperator;
                    $scope.pendingRequestChat = true;
                } else {
                    $scope.myHub.clientSendMessage(msg, userName);
                };
                $scope.message = "";
            };
            $scope.acceptRequestChat = function (customerConnectionId, firstComment, customerName) {
                $scope.myHub.acceptRequestChat(customerConnectionId, firstComment, customerName);
            };
            $scope.changeAgentStatus = function () {
                $scope.agent.isOnline = !$scope.agent.isOnline;
                $scope.myHub.agentChangeStatus($scope.agent.isOnline);
            };
            $scope.detailsChat = function (chatId, userName) {
                $scope.agentUserMsgs = [];
                angular.forEach($scope.chatConversation, function (index) {
                    if (index.id === chatId) {
                        $scope.dateStartChat = index.dateStartChat;
                        angular.forEach(index.sessions, function (value) {
                            $scope.agentUserMsgs.push({ name: value.name, msg: value.msg, date: value.date });
                        });
                    };
                });
                $scope.agentChatWithUser = chatId;
                $scope.customerName = userName;
                $("#agentUserChat").modal();
            };
            $scope.ticket = {
                submit: function () {
                    var name = $scope.ticket.name;
                    var email = $scope.ticket.email;
                    var comment = $scope.ticket.comment;
                    $scope.myHub.sendTicket(name, email, comment);
                }
            };
            $scope.showHistory = function () {
                $scope.myHub.getAgentHistoryChat($scope.agent.name);
            };
            $scope.detailsChatHistory = function (id) {
                $scope.myHub.detailsSessoinMessage(id, $scope.agent.id);
            };
            $scope.agentMsgToUser = function (msg) {
                var chatId = $scope.agentChatWithUser;
                var customerName = $scope.customerName;
                if (!customerName) {
                    angular.forEach($scope.customerVisit, function (index) {
                        if (index.connectionId == chatId) {
                            customerName = index.userName;
                        }
                    });
                }
                if (chatId !== "" && msg !== "") {
                    $scope.myHub.agentSendMessage(chatId, msg, customerName);
                }
                //not bind to scope.msg! not correctly work
                $scope.msg = "";
                $("#post-msg").val("");
            };
            $scope.closeChat = function (chatId) {
                var item = $scope.chatConversation[getid(chatId)];
                $scope.myHub.closeChat(chatId);
            };
            $scope.engageVisitor = function (newAgentId) {
                var customerId = $scope.customerId;
                var customerName = $scope.customerName;
                var clientSessionId = $scope.clientSessionId;
                $scope.myHub.engageVisitor(newAgentId, customerId, customerName, clientSessionId);
                $("[data-dismiss=modal]").trigger({ type: "click" });
            };
            $scope.selectVisitor = function (customerId, customerName, clientSessionId) {
                $scope.customerId = customerId;
                $scope.customerName = customerName;
                $scope.clientSessionId = clientSessionId;
                $scope.myHub.showAgentList();
            };
            $scope.setClass = function (item) {
                if (item === "من")
                    return "question";
                else
                    return "response";
            };
            $scope.setdirectionClass = function (item) {
                if (item === $scope.agent.name)
                    return { "float": "left" };
                else
                    return { "float": "right" };
            };
            $scope.setArrowClass = function (item) {
                if (item === $scope.agent.name)
                    return "left-arrow";
                else
                    return "right-arrow";
            };
            $scope.setAlarm = function () {
            $scope.alarmStatus = !$scope.alarmStatus;
            };
        }
    }]);
app.directive("showtab", function () {
    return {
        link: function (scope, element, attrs) {
            element.click(function (e) {
                e.preventDefault();
                $(element).addClass("active");
                $(element).tab("show");
            });
        }
    };
});
//زمان ارسال پیام
app.directive("timeAgo", function ($q) {
    return {
        restrict: "AE",
        scope: false,
        link: function (scope, element, attrs) {
            jQuery.timeago.settings.strings =
            {
                prefixAgo: null,
                prefixFromNow: null,
                suffixAgo: "پیش",
                suffixFromNow: "از حالا",
                seconds: "کمتر از یک دقیقه",
                minute: "در حدود یک دقیقه",
                minutes: "%d دقیقه",
                hour: "حدود یگ ساعت",
                hours: "حدود %d ساعت ",
                day: "یک روز",
                days: "%d روز",
                month: "حدود یک ماه",
                months: "%d ماه",
                year: "حدود یک سال",
                years: "%d سال",
                wordSeparator: " ",
                numbers: []
            }
            var parsedDate = $q.defer();
            parsedDate.promise.then(function () {
                jQuery(element).timeago();
            });
            attrs.$observe("title", function (newValue) {
                parsedDate.resolve(newValue);
            });
        }
    };
});
فایل chathub.cs
برای آشنایی بیشتر مقاله نگاهی به SignalR Hubs   مفید خواهد بود.
   [HubName("chatHub")]
    public class ChatHub : Microsoft.AspNet.SignalR.Hub
    {
        private readonly ApplicationDbContext _db = new ApplicationDbContext();
        private static ConcurrentDictionary<string, UserInformation> _agents;
        private static List<ChatSessionVm> _chatSessions;
        private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver(),
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        };
        public void Init()
        {
            _chatSessions = _chatSessions ?? (_chatSessions = new List<ChatSessionVm>());
            _agents = _agents ?? (_agents = new ConcurrentDictionary<string, UserInformation>());
            Clients.Caller.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
        }
        public void AgentConnect(string userName)
        {
            //ما برای ساده کردن مقایسه ساده ای انجام دادیم فقط کاربر حسین یا علی میتواند کارشناس باشد
            if (userName == "hossein" || userName == "ali")
            {
                var agent = new UserInformation();
                if (_agents.Any(item => item.Key == userName))
                {
                    agent = _agents[userName];
                    agent.ConnectionId = Context.ConnectionId;
                }
                else
                {
                    agent.ConnectionId = Context.ConnectionId;
                    agent.UserName = userName;
                    agent.IsOnline = true;
                    _agents.TryAdd(userName, agent);

                }
                Clients.Caller.loginResult(true, agent.ConnectionId, agent.UserName);
                Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
            }
            else
            {
                Clients.Caller.loginResult(false, null, null);
            }
        }
        public void AgentChangeStatus(bool status)
        {
            var agent = _agents.FirstOrDefault(x => x.Value.ConnectionId == Context.ConnectionId).Value;
            if (agent == null) return;
            agent.IsOnline = status;
            Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
        }
        public void LogVisit(string city, string country, string firstComment, string userName)
        {
            foreach (var agent in _agents)
            {
                Clients.Client(agent.Value.ConnectionId).newVisit(userName, city, country, null, Context.ConnectionId, firstComment);
            }
        }
        public void AcceptRequestChat(string customerConnectionId, string body, string userName)
        {
            var agent = FindAgent(Context.ConnectionId);
            var session = _chatSessions.FirstOrDefault(item => item.Key.Equals(agent.Key));
            if (session == null)
            {
                _chatSessions.Add(new ChatSessionVm
                {
                    Key = agent.Key,
                    Value = new List<string> { customerConnectionId }
                });
            }
            else
            {
                session.Value.Add(customerConnectionId);
            }
            Clients.Client(Context.ConnectionId).agentChat(customerConnectionId, body, userName);
            Clients.Client(customerConnectionId).clientChat(customerConnectionId, agent.Value.UserName);
            foreach (var item in _agents.Where(item => item.Value.IsOnline))
            {
                Clients.Client(item.Value.ConnectionId).refreshChatWith(agent.Value.UserName, customerConnectionId);
            }
            _db.Sessions.Add(new Session
            {
                AgentName = agent.Key,
                CustomerName = userName,
                CreatedDateTime = DateTime.Now
            });
            _db.SaveChanges();

            var message = new Message
            {
                CreationTime = DateTime.Now,
                Sender = agent.Key,
                Receiver = userName,
                Body = body,
                //ConnectionId = _agents.FirstOrDefault(item => item.Value.UserName == userName).Key,
                Session = _db.Sessions.OrderByDescending(item => item.Id)
                .FirstOrDefault(item => item.AgentName.Equals(agent.Key) && item.CustomerName.Equals(userName))
            };
            _db.Messages.Add(message);
            _db.SaveChanges();
        }
        public void GetAgentHistoryChat(string userName)
        {
            var dic = new Dictionary<int, int>();
            var lenght = 0;
            var chats = _db.Sessions.OrderBy(item => item.Id).Include(item => item.Parent)
                .Where(item => item.AgentName.Equals(userName)).ToList();

            foreach (var session in chats)
            {
                Result(session, ref lenght);
                dic.Add(session.Id, lenght);
                lenght = 0;
            }
            if (!chats.Any()) return;

            var historyResult = chats.Select(item => new AgentViewModel
            {
                Id = item.Id,
                CustomerName = item.CustomerName,
                Date = item.CreatedDateTime,
                Lenght = dic.Any(di => di.Key.Equals(item.Id)) ? dic.FirstOrDefault(di => di.Key.Equals(item.Id)).Value : 0,
            }).OrderByDescending(item => item.Id).ToList();
            Clients.Caller.receiveHistory(JsonConvert.SerializeObject(historyResult, new Formatting(), _settings));
        }
        public void DetailsSessoinMessage(int sessionId, string agentId)
        {
            var session = _db.Sessions.FirstOrDefault(item => item.Id.Equals(sessionId));
            if (session == null) return;
            var list = new List<Message>();
            GetAllMessages(session, list);
            var result = JsonConvert.SerializeObject(list.OrderBy(item => item.Id), new Formatting(), _settings);
            Clients.Client(Context.ConnectionId).detailsHistory(result);
        }
        public void ClientSendMessage(string body, string userName)
        {
            var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(Context.ConnectionId));
            if (session == null || session.Key == null) return;
            var agentId = _agents.FirstOrDefault(item => item.Key.Equals(session.Key)).Value.ConnectionId;
            Clients.Caller.clientAddMessage("من", body);
            Clients.Client(agentId).addMessage(Context.ConnectionId, userName, body);
            var message = new Message
            {
                Sender = FindAgent(agentId).Key,
                Receiver = userName,
                Body = body,
                CreationTime = DateTime.Now,
                Session = FindSession(userName, FindAgent(agentId).Key)
            };
            _db.Messages.Add(message);
            _db.SaveChanges();
        }
        public void AgentSendMessage(string id, string body, string userName)
        {
            var agent = FindAgent(Context.ConnectionId);
            Clients.Caller.addMessage(id, agent.Value.UserName, body);
            Clients.Client(id).clientAddMessage(agent.Value.UserName, body);
            var message = new Message
            {
                Sender = agent.Key,
                Receiver = userName,
                Body = body,
                Session = FindSession(agent.Key, userName),
                CreationTime = DateTime.Now
            };
            _db.Messages.Add(message);
            _db.SaveChanges();
        }
        public void CloseChat(string id)
        {
            var findAgent = FindAgent(Context.ConnectionId);
            var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
            if (session == null) return;
            Clients.Client(id).clientAddMessage(findAgent.Key, "مکالمه شما با کارشناس مربوطه به اتمام رسیده است");

            foreach (var agent in _agents)
            {
                Clients.Client(agent.Value.ConnectionId).refreshLeaveChat(agent.Value.UserName, id);
            }
            _chatSessions.Remove(session);
        }
        public void RequestChat(string message)
        {
            Clients.Caller.clientAddMessage("من", message);
        }
        public void EngageVisitor(string newAgentId, string cumtomerId, string customerName,string clientSessionId)
        {
            #region remove session of current agent
            var currentAgent = FindAgent(Context.ConnectionId);
            var currentSession = _chatSessions.FirstOrDefault(item => item.Value.Contains(cumtomerId));
            if (currentSession != null)
            {
                _chatSessions.Remove(currentSession);
            }
            #endregion

            #region add  session to new agent
            var newAgent = FindAgent(newAgentId);
            var newSession = _chatSessions.FirstOrDefault(item => item.Key.Equals(newAgent.Key));
            if (newSession == null)
            {
                _chatSessions.Add(new ChatSessionVm
                {
                    Key = newAgent.Key,
                    Value = new List<string> { cumtomerId }
                });
            }
            else
            {
                newSession.Value.Add(cumtomerId);
            }
            #endregion

            Clients.Client(currentAgent.Value.ConnectionId).addMessage(cumtomerId, newAgent.Key,
                "ادامه مکالمه به کارشناس  " + newAgent.Key + "مقابل  منتقل شد");
            Clients.Client(newAgentId).addMessage(cumtomerId, currentAgent.Key,
                "لطفا مکالمه را ادامه دهید.با تشکر");

            Clients.Client(cumtomerId).clientAddMessage(newAgent.Value.UserName,
                "مکالمه شما با کارشناس زیر برقرار گردید" + newAgent.Key);

            var session = _db.Sessions.FirstOrDefault
                (item => item.AgentName.Equals(currentAgent.Value.UserName)
                 && item.CustomerName.Equals(customerName));
            if (session != null)
            {
                var sessionId = session.Id;
                var messages = _db.Messages.Where(item => item.Session.Id.Equals(sessionId));
                var result = JsonConvert.SerializeObject(messages, new Formatting(), _settings);
                Clients.Client(newAgentId).visitorSwitchConversation
                    (Context.ConnectionId, customerName, result, clientSessionId);
            }
            foreach (var item in _agents.Where(item => item.Value.IsOnline))
            {
                Clients.Client(item.Value.ConnectionId).refreshChatWith(newAgent.Value.UserName, cumtomerId);
            }
            _db.Sessions.Add(new Session
            {
                AgentName = newAgent.Key,
                CustomerName = customerName,
                CreatedDateTime = DateTime.Now,
                Parent = _db.Sessions.Where(item => item.AgentName.Equals(currentAgent.Key)
                      && item.CustomerName.Equals(customerName)).OrderByDescending(item => item.Id).FirstOrDefault()
            });
            _db.SaveChanges();
        }
        public void ShowAgentList()
        {
            Clients.Caller.agentList(_agents.ToList());
        }
        public override Task OnDisconnected(bool stopCalled)
        {
            var id = Context.ConnectionId;
            var isAgent = _agents != null && _agents.Any(item => item.Value.ConnectionId.Equals(id));
            if (isAgent)
            {
                UserInformation agent;
                var currentAgentConnectionId = FindAgent(id).Key;
                if (currentAgentConnectionId == null)
                    return base.OnDisconnected(stopCalled);
                if (_chatSessions.Any())
                {
                    var sessions = _chatSessions.FirstOrDefault(item => item.Key.Equals(currentAgentConnectionId));
                    //اطلاع دادن به تمام کاربرانی که در حال مکالمه با کارشناس هستند
                    if (sessions != null)
                    {
                        var result = sessions.Value.ToList();
                        for (var i = 0; i < result.Count(); i++)
                        {
                            var localId = result[i];
                            Clients.Client(localId).clientAddMessage(currentAgentConnectionId, "ارتباط شما با مشاور مورد نظر قطع شده است");
                        }
                    }
                }
                _agents.TryRemove(currentAgentConnectionId, out agent);
                Clients.All.onlineStatus(_agents.Count(x => x.Value.IsOnline) > 0);
                Clients.Client(id).loginResult(false, null, null);
            }
            else
            {
                if (_chatSessions == null ||
                    !_chatSessions.Any(item => item.Value.Contains(id)
                        && _agents == null))
                    return base.OnDisconnected(stopCalled);

                var session = _chatSessions.FirstOrDefault(item => item.Value.Contains(id));
                if (session == null)
                    return base.OnDisconnected(stopCalled);

                var agentName = session.Key;
                var agent = _agents.FirstOrDefault(item => item.Key.Equals(agentName));
                if (agent.Key != null)
                {
                    Clients.Client(agent.Value.ConnectionId).addMessage(id, "کاربر", "اتصال با کاربر قطع شده است");
                }
            }
            return base.OnDisconnected(stopCalled);
        }


        private KeyValuePair<string, UserInformation> FindAgent(string connectionId)
        {
            return _agents.FirstOrDefault(item => item.Value.ConnectionId.Equals(connectionId));
        }
        private Session FindSession(string key, string userName)
        {
            return _db.Sessions.Where(item => item.AgentName.Equals(key) && item.CustomerName.Equals(userName))
                .OrderByDescending(item => item.Id).FirstOrDefault();
        }
        private static void Result(Session parent, ref int lenght)
        {
            while (true)
            {
                if (parent == null)
                    return;
                lenght += parent.Messages.Count();
                parent = parent.Parent;
            }
        }
        private static List<Message> GetAllMessages(Session node, List<Message> list)
        {
            if (node == null) return null;
            list.AddRange(node.Messages);
            if (node.Parent != null)
            {
                GetAllMessages(node.Parent, list);
            }
            return null;
        }
    }
فایل agent.html
<div>
    <div>
        <h2>
            خوش آمدید
            <span ng-bind="agent.name">
            </span>
            <a ng-click="changeAgentStatus()">
                <i ng-if="changeStatus==null"
                   data-placement="bottom"
                   data-trigger="hover "
                   bs-tooltip="options.lock"></i>
                <i ng-if="changeStatus==true"
                   data-placement="bottom"
                   data-trigger="hover"
                   bs-tooltip="options.unlock"></i>
            </a>
        </h2>
        <div style="float: left">
            <a ng-click="setAlarm()">
                <i ng-show="alarmStatus"
                   data-placement="bottom"
                   data-trigger="hover "
                   bs-tooltip="options.alarmOn"></i>

                <i ng-show="!alarmStatus"
                   data-placement="bottom"
                   data-trigger="hover "
                   bs-tooltip="options.alarmOff"></i>
            </a>
            <!--<a data-placement="bottom"
               data-trigger="hover "
               bs-tooltip="options.conversion" ng-click="showHistory()"><i></i></a>-->

            <a data-placement="bottom"
               data-trigger="hover "
               bs-tooltip="options.edit"><i></i><span></span></a>

            <a data-placement="bottom"
               data-trigger="hover "
               bs-tooltip="options.setting"><i></i></a>

            <a data-placement="bottom"
               data-trigger="hover "
               bs-tooltip="options.signOut" ng-click="LeaveChat()"><i></i><span></span></a>
        </div>
    </div>
    <div>
        <div>
            <div id="chat-content">
                <div>
                    <ul>
                        <li>
                            <a showtab href="#online-list">آنلاین</a>
                        </li>
                        <li>
                            <a ng-click="showHistory()" showtab href="#conversation">آرشیو گفتگوها</a>
                        </li>
                    </ul>
                    <div>
                        <div id="online-list">
                            <div>
                                <h2>
                                    <i></i><span></span>
                                    <span>نمایش آنلاین مراجعه ها</span>
                                </h2>
                            </div>
                            <div>
                                <div id="agent-chat">
                                    <div id="real-time-visits">
                                        <table id="current-visits">
                                            <thead>
                                                <tr>
                                                    <th>نام کاربر</th>
                                                    <th>زمان اولین تقاضا</th>
                                                    <th>منطقه</th>
                                                    <th>پاسخ</th>
                                                </tr>
                                            </thead>
                                            <tbody>
                                                <tr id="{{item.connectionId}}" ng-animate="animate" ng-repeat="item in customerVisit ">
                                                    <td ng-bind="item.userName"></td>
                                                    <td>
                                                        <span time-ago title="{{item.date}}"></span>
                                                    </td>
                                                    <td>
                                                        <span ng-bind="item.country"></span> /<span ng-bind="item.city"> </span>
                                                    </td>
                                                    <td>
                                                        <a style="cursor: pointer" ng-if="item.chatWith== null"
                                                           ng-click="acceptRequestChat(item.connectionId,item.firstComment,item.userName)">
                                                            شروع مکالمه
                                                        </a>
                                                        <span ng-if="item.chatWith ">
                                                            وضعیت:
                                                            <span>در حال مکالمه با</span>
                                                            <span ng-bind="item.chatWith"></span>
                                                            <a ng-show="item.chatWith==agent.name"
                                                              
                                                               ng-click="selectVisitor(item.connectionId,item.userName,item.connectionId)">
                                                                انتقال مکالمه
                                                            </a>
                                                        </span>
                                                        <ul ng-repeat="session in chatSessions track by $index" style="padding:0px;">
                                                            <li ng-if="session.id==item.connectionId" id="{{session.id}}">
                                                                <div>
                                                                    <p>
                                                                        تاریخ شروع مکالمه:
                                                                        <span time-ago title="{{session.date}}"></span>
                                                                    </p>
                                                                    <p>
                                                                        تعداد پیام ها:
                                                                        <span ng-bind="session.length"></span>
                                                                    </p>
                                                                </div>
                                                                <p>
                                                                    <a ng-click="detailsChat(session.id,session.userName)">جزییات </a>
                                                                    <a ng-click="closeChat(session.id)">
                                                                        خاتمه عملیات
                                                                    </a>
                                                                </p>
                                                            </li>
                                                        </ul>
                                                    </td>
                                                </tr>
                                            </tbody>
                                        </table>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div id="conversation">
                            <div>
                                <h2>
                                    <i></i><span></span>
                                    <span>آرشیو گفتگوهای </span>
                                    {{agent.name}}
                                </h2>
                            </div>
                            <div>
                                <div>
                                    <table id="current-visits">
                                        <thead>
                                            <tr>
                                                <th>شناسه مشتری</th>
                                                <th>نام مشتری</th>
                                                <th>تعداد محاوره ها</th>
                                                <th>تاریخ</th>
                                                <th>جزئیات</th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr ng-repeat="item in agentHistory track by $index">
                                                <td ng-bind="item.id"></td>
                                                <td ng-bind="item.customerName"></td>
                                                <th ng-bind="item.lenght"></th>
                                                <td><span time-ago title="{{item.date}}"></span></td>
                                                <th>
                                                    <ang-click="detailsChatHistory(item.id)" >مشاهده جزییات گفتگو</a>
                                                </th>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div id="detailsAgentHistory" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
        <div>
            <div>
                <div>
                    <div>
                        <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                    </div>
                    <h2>
                        <span></span>تاریخچه گفتگو
                    </h2>
                </div>
                <div>
                    <div style="display: block">
                        <ul ng-repeat="item in historyMsg">
                            <li>
                                <span ng-bind="item.name" ng-style="setdirectionClass(item.name)">
                                </span>
                                <span ng-style="setdirectionClass(item.name)">
                                    <span ng-class="setArrowClass(item.name)"></span>
                                    <span time-ago title="{{item.date}}"></span>
                                    <span>
                                        <p ng-bind-html="item.msg | smilies"></p>
                                    </span>
                                </span>
                            </li>
                        </ul>
                    </div>

                </div>
            </div>
        </div>
    </div>
    <div id="agentList" tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
        <div>
            <div>
                <div>
                    <div>
                        <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                    </div>
                    <h2>
                        <span></span>لیست تمام کارشناسان
                    </h2>
                </div>
                <div>
                    <div style="display: block;">
                        <div ng-show="agentList.length==0">
                            کارشناس آنلاینی وجود ندارد
                        </div>
                        <ul ng-repeat="item in agentList">
                            <li>
                                <span>
                                    <a ng-click="engageVisitor(item.id)">{{item.name}}</a>
                                </span>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div id="agentUserChat"  tabindex="-1" role="dialog" aria-labelledby="cmdLabel" aria-hidden="true">
        <div>
            <div>
                <div>
                    <div>
                        <button type="button" data-dismiss="modal" aria-hidden="true">×</button>
                    </div>
                    <h2>
                        <span></span>گفتگو
                    </h2>
                </div>
                <div>
                    <div>
                        <div>
                            <div style="display: block;">
                                <label>شروع چت در </label>:
                                <span ng-bind="dateStartChat"></span>

                                <ul>
                                    <li ng-repeat="item in agentUserMsgs">
                                        <span ng-bind="item.name" ng-style="setdirectionClass(item.name)">

                                        </span>
                                        <span ng-style="setdirectionClass(item.name)">
                                            <span ng-class="setArrowClass(item.name)"></span>
                                            <span time-ago title="{{item.date}}"></span>

                                            <span>
                                                <p ng-bind-html="item.msg | smilies"></p>
                                            </span>
                                        </span>
                                    </li>
                                </ul>
                                <div>
                                    <div>
                                        <textarea id="post-msg" ng-model="msg" placeholder="متن خود را وارد نمایید" style="overflow: hidden; word-wrap: break-word; resize: horizontal; height: 80px; max-width: 100%"></textarea>
                                        <span smilies-selector="msg" smilies-placement="right" smilies-title="Smilies"></span>
                                    </div>
                                    <div style="text-align: center; margin-top: 5px">
                                        <button ng-click="agentMsgToUser(msg)">ارسال</button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
فایل index.cshtml
<html ng-app="app">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Live Support</title>

    <link href="~/Content/bootstrap-rtl.css" rel="stylesheet" />
    <link href="~/Scripts/smilies/angular-smilies-embed.css" rel="stylesheet" />
    <link href="~/Content/font-awesome.css" rel="stylesheet" />

    <link href="~/Content/toastr.css" rel="stylesheet" />
    <link href="~/Content/liveSupport.css" rel="stylesheet" />

    <script src="~/Scripts/jquery-1.10.2.js"></script>
    <script src="~/Scripts/toastr.js"></script>
    <script src="~/Scripts/jquery.timeago.js"></script>

    <script src="~/Scripts/angular.js"></script>
    <script src="~/Scripts/angular-animate.js"></script>
    <script src="~/Scripts/angular-sanitize.js"></script>
    <script src="~/Scripts/angular-route.js"></script>

    <script src="~/Scripts/angular-strap.js"></script>
    <script src="~/Scripts/angular-strap.tpl.js"></script>

    <script src="~/Scripts/smilies/angular-smilies.js"></script>

    <script src="~/Scripts/jquery.signalR-2.2.0.js"></script>
    <script src="~/Scripts/angular-signalr-hub.js"></script>

    <script src="~/app/app.js"></script>
    @Scripts.Render("~/bundles/bootstrap")

</head>
<body ng-controller="ChatCtrl">
    <div ng-view>
    </div>

    <div id="chat-box-header" ng-click="boxheader()">
        {{chatTitle}}
    </div>
    <div id="chat-box">
        <div ng-show="hasOnline">
            <div id="style-1" style="min-height:100px;">
                <div ng-repeat="item in clientAgentMsg  track by $index">
                    <span ng-class="setClass(item.name)">
                        {{item.name}}
                    </span>
                    <br />
                    <p ng-bind-html="item.msg | smilies"></p>
                </div>
            </div>
            <div>
                <label>پیام</label>
                <div style="text-align: left; clear: both">
                    <a data-placement="top"
                       data-trigger="hover "
                       bs-tooltip="options.alarm" ng-click="alarm()"><i></i></a>
                    <a data-placement="top"
                       data-trigger="hover "
                       bs-tooltip="options.signOut" href="signOut()"><i></i><span></span></a>
                    <a data-placement="top"
                       data-trigger="hover "
                       bs-tooltip="options.upload" href="fileupload()">
                        <span><i></i></span>
                    </a>
                </div>
                <div>
                    <textarea style="height: 150px; max-height: 160px;" ng-model="message" placeholder=" متن خود را وارد نمایید"></textarea>
                    <span smilies-selector="message" smilies-placement="right" smilies-title="Smilies"></span>
                </div>
            </div>
            <div style="text-align: center">
                <button type="button" ng-disabled="pendingRequestChat" ng-click="requestChat(message)">ارسال </button>
            </div>
        </div>
        <div ng-show="hasOffline">
            <div>
                <form name="Ticket" id="form1">
                    <fieldset>
                        <div>
                            <label>نام</label>
                            <input name="email"
                                   ng-model="ticket.name"
                                  >
                        </div>
                        <div>
                            <label>ایمیل</label>
                            <input name="email"
                                   ng-model="ticket.email"
                                  >
                        </div>
                        <div>
                            <label>پیام</label>
                        </div>
                        <div>
                            <textarea ng-model="ticket.comment" placeholder="متن خود را وارد نمایید"></textarea>
                            <span smilies-selector="ticket.comment" smilies-placement="right" smilies-title="Smilies"></span>
                        </div>
                    </fieldset>
                    <div style="text-align: center">
                        <button type="button"
                                ng-click="ticket.submit(ticket)">
                            ارسال
                        </button>
                    </div>
                </form>
            </div>
        </div>
    </div>
   
</body>

</html>
LiveSupport.zip 

نکات تکمیلی :
نگاشت  Hub‌ها به برنامه در مسیر ("signalr /") در فایل  ConfigureAuth.Cs 
  app.MapSignalR();

نظرات مطالب
آموزش (jQuery) جی کوئری 5#
سلام؛ من یه اسلاید شو ساده را از اموزشهای یه سایت انگلیسی زبان ساختم که مدتهاست دنبالش بودم
اما هنوز یه ایراد کوچولو داره و اونهم اینه که بعد از رسیدن به اخرین عکس برمیگرده به اول یعنی بصورت بک اسلاید میشه و اگر عکسها از سمت راست به چپ اسلاید میشوند وقتی به اخرین عکس میرسه تمام عکسها در کسری از ثانیه از چپ به راست برمیگردند. نمونه کد کوئری رو میزارم و ممنون میشم منو در این زمینه راهنمایی کنید که چطور کاری کنم با رسیدن به اخرین عکس به همون روش از سمت راست به چپ دوباره برگرده به عکس اول نه تمام عکسها رو از چپ به راست برگردونه ؟
اسکریپت فراخوانده شده :
  < script src = "http://code.jquery.com/jquery-latest.js" ></ script >
 کوئری نوشته شده : 
<script type = "text/javascript" >
$(document).ready(function () { 
   slideShow();
});
 
var n = 0;
function slideShow() {
   id = n % 5 + 1;
   leftpost = (1 - parseInt(id)) * 500 + "px";
   $("div.slider-item").animate({ left: leftpost }, 1500);
   n = n + 1;
   s = setTimeout("slideShow()", 3000);
}
 </ script >
فایل css :
 

<style type = "text/css" >
div#slider {
  width: 500px;
  height: 300px;
  margin: auto;
  overflow: hidden;
  border: 10px solid gray;
}
div#slider-mask {
  width: 500%;
  height: 100%;
}
div.slider-item {
  width: 20%;
  height: 100%;
  position: relative;
  float: left;
}
 </ style >
فایل html :
  < div id = "slider" > < div id = "slider-mask" > 
< div class = "slider-item" >< img src = "img1.jpg" alt = "1" /></ div > 
< div class = "slider-item" >< img src = "img2.jpg" alt = "2" /></ div > 
< div class = "slider-item" >< img src = "img3.jpg" alt = "3" /></ div >
 < div class = "slider-item" >< img src = "img4.jpg" alt = "4" /></ div > 
< div class = "slider-item" >< img src = "img5.jpg" alt = "5" /></ div >
</ div > </ div >
مطالب
Blazor 5x - قسمت 27 - برنامه‌ی Blazor WASM - کار با سرویس‌های Web API
در قسمت‌های Blazor Server مثال این سری، با روش کار با سرویس‌های سمت سرور برنامه، آشنا شدیم. در این نوع برنامه‌ها، فقط کافی است اصل سرویس مدنظر را مستقیما در کامپوننت‌های Razor تزریق کرد و سپس می‌توان به نحو متداولی با آن‌ها کار کرد؛ اما در برنامه‌های Blazor WASM خیر! به این نوع برنامه‌های سمت کلاینت باید همانند برنامه‌های React ، Angular ، Vue و یا حتی برنامه‌های مبتنی بر jQuery نگاه کرد. در تمام فناوری‌های سمت کلاینت، این درخواست‌های Ajax ای هستند که با سرویس‌های یک Web API سمت سرور، ارتباط برقرار کرده، اطلاعاتی را به آن‌ها ارسال و یا دریافت می‌کنند. در برنامه‌های Blazor WASM نیز باید به همین ترتیب عمل کرد و در اینجا HttpClient دات نت، جایگزین برای مثال jQuery Ajax ، Fetch API و یا XMLHttpRequest استاندارد می‌شود (البته jQuery Ajax در اصل یک محصور کننده‌ی استاندارد XMLHttpRequest است که برای اولین بار توسط مایکروسافت در برنامه‌ی Outlook web access معرفی شد).


ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاق‌ها از Web API

در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاق‌ها و یا یک اتاق مشخص به کلاینت است. اکنون می‌خواهیم در ادامه‌ی قسمت قبل، اگر کاربری بر روی دکمه‌ی Go صفحه‌ی اول رزرو اتاقی کلیک کرد، لیست تمام اتاق‌های تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
namespace BlazorWasm.Client.Services
{
    public interface IClientHotelRoomService
    {
        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate);
        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate);
    }
}
این سرویس را در پوشه‌ی Services پروژه‌ی BlazorWasm.Client ایجاد کرده‌ایم که HotelRoomDTO خود را از پروژه‌ی BlazorServer.Models دریافت می‌کند. به این ترتیب می‌توان مدلی را بین یک Web API سمت سرور و یک سرویس سمت کلاینت، به اشتراک گذاشت. بنابراین پروژه‌ی کلاینت، باید ارجاعی را به پروژه‌ی BlazorServer.Models.csproj نیز داشته باشد.

در ادامه اینترفیس فوق را به صورت زیر پیاده سازی می‌کنیم:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            throw new NotImplementedException();
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, "/api/hotelroom"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<IEnumerable<HotelRoomDTO>>(uri);
        }
    }
}
توضیحات:
- HttpClient یکی از سرویس‌های تنظیم شده‌ی در فایل Program.cs پروژه‌های سمت کلاینت است. بنابراین می‌توان آن‌را از طریق تزریق به سازنده‌ی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شده‌است. این مورد جزو تازه‌های NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده می‌کنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using BlazorServer.Models;
using BlazorWasm.Client.Utils;

using System;
using System.Collections.Specialized;
using System.Web;

namespace BlazorWasm.Client.Utils
{
    public class UriBuilderExt
    {
        private readonly NameValueCollection _collection;
        private readonly UriBuilder _builder;

        public UriBuilderExt(Uri uri)
        {
            _builder = new UriBuilder(uri);
            _collection = HttpUtility.ParseQueryString(string.Empty);
        }

        public UriBuilderExt AddParameter(string key, string value)
        {
            _collection.Add(key, value);
            return this;
        }

        public Uri Uri
        {
            get
            {
                _builder.Query = _collection.ToString();
                return _builder.Uri;
            }
        }
    }
}
- در اینجا توسط متد AddParameter، کار افزودن کوئری استرینگ‌ها به یک Url از پیش مشخص، انجام می‌شود. کار encoding نهایی به صورت خودکار توسط HttpUtility استاندارد دات نت، انجام خواهد شد.
- تاریخ‌های ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالب‌های پذیرفته شده‌است.
- جهت سهولت استفاده‌ی از سرویس فوق و همچنین مدل‌های برنامه، فضای نام آن‌ها را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم تا در تمام کامپوننت‌های برنامه‌ی سمت کلاینت، قابل دسترسی شوند:
@using BlazorWasm.Client.Services
@using BlazorServer.Models
- در آخر این سرویس جدید را باید به لیست سرویس‌های برنامه معرفی کرد تا قابلیت تزریق در کامپوننت‌ها را پیدا کند:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddScoped<IClientHotelRoomService, ClientHotelRoomService>();

            // ...
        }
    }
}

چند اصلاح جزئی در کنترلرها و سرویس‌های سمت سرور

در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class HotelRoomController : ControllerBase
    {
        // ...

        [HttpGet]
        public async Task<IActionResult> GetHotelRooms(DateTime? checkInDate, DateTime? checkOutDate)
        {
          // ...
        }

        [HttpGet("{roomId}")]
        public async Task<IActionResult> GetHotelRoom(int? roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
           // ...
        }
    }
}
و همچنین سرویس سمت سرور IHotelRoomService نیز اضافه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomService : IDisposable
    {
        Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDate, DateTime? checkOutDate);
        Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate);
        // ...
    }
}
البته فعلا پیاده سازی خاصی ندارند و آن‌ها را در قسمت‌های بعد مورد استفاده قرار خواهیم داد.


تنظیمات ویژه‌ی HttpClient برنامه‌ی سمت کلاینت

سرویس ClientHotelRoomService فوق، از HttpClient تزریق شده‌ی در سازنده‌ی خود استفاده می‌کند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت می‌کند. در اینجا علاقمندیم تا بجای این تنظیم پیش‌فرض، فایل جدید appsettings.json را به پوشه‌ی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامه‌های سمت کلاینت، داخل پوشه‌ی wwwroot است و نه در داخل پوشه‌ی ریشه‌ی اصلی پروژه):
{
    "BaseAPIUrl": "https://localhost:5001/"
}
و از این تنظیم جدید به عنوان BaseAddress برنامه‌ی Web API استفاده کنیم که روش آن‌را در کدهای ذیل مشاهده می‌کنید:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ... 

            // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddScoped(sp => new HttpClient
            {
                BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl"))
            });

            // ... 
        }
    }
}

تکمیل کامپوننت دریافت لیست تمام اتاق‌ها

در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمه‌ی Go صفحه‌ی اول، به این کامپوننت هدایت می‌شوند. اکنون می‌خواهیم، لیست تمام اتاق‌ها را در این کامپوننت، از Web API برنامه دریافت کنیم:
@page "/hotel/rooms"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime
@inject IClientHotelRoomService HotelRoomService

<h3>HotelRooms</h3>

@code {
    HomeVM HomeModel = new HomeVM();
    IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>();

    protected override async Task OnInitializedAsync()
    {
        try
        {
            var model = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
            if (model is not null)
            {
                HomeModel = model;
            }
            else
            {
                HomeModel.NoOfNights = 1;
            }
            await LoadRooms();
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    private async Task LoadRooms()
    {
        Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate);
    }
}
در اینجا در ابتدا سعی می‌شود تا HomeModel، از Local Storage که در قسمت قبل آن‌را تنظیم کردیم، خوانده شود. سپس با استفاده از متد GetHotelRoomsAsync، لیست اتاق‌ها را از Web API دریافت می‌کنیم. تمام این عملیات آغازین نیز باید در روال رویدادگران OnInitializedAsync صورت گیرند.


روش اجرای پروژه‌های Blazor WASM

تا اینجا اگر برنامه‌ی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحه‌ی خالی نمایش لیست اتاق‌ها ظاهر می‌شود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژه‌ی Web API را اجرا کرد و سپس پروژه‌ی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده می‌کنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینه‌ی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینه‌ی «multiple startup projects» را انتخاب کرده و از لیست پروژه‌های موجود، دو پروژه‌ی Web API و WASM را انتخاب کنید و Action مقابل آن‌ها را به Start تنظیم کنید. در اینجا حتی می‌توان ترتیب اجرای این پروژه‌ها را هم تغییر داد. در این حالت زمانیکه بر روی دکمه‌ی Run، در ویژوال استودیو کلیک می‌کنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.

نکته‌ی مهم! در این حالت هم برنامه‌ی کلاینت نمی‌تواند با برنامه‌ی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شده‌ی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژه‌ی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
{
  "iisSettings": {
    "iisExpress": {
      "applicationUrl": "http://localhost:62930",
      "sslPort": 44314
    }
  }
}
و یا اگر می‌خواهید پروژه را از طریق NET Core CLI. با اجرای دستور dotnet watch run اجرا کنید ... به صورت پیش‌فرض نمی‌شود! چون برای اینکار باید به پوشه‌ی ریشه‌ی پروژه‌های Web API و WASM وارد شد و دوبار دستور یاد شده را به صورت مجزا اجرا کرد. در این حالت، هر دو پروژه، بر روی پورت پیش‌فرض 5001 اجرا می‌شوند. روش تغییر این پورت، مراجعه به فایل Properties\launchSettings.json این پروژه‌ها است. برای مثال همان پورت پیش‌فرض 5001 را که در فایل appsettings.json انتخاب کردیم، ثابت نگه می‌داریم. یعنی فایل launchSettings.json پروژه‌ی Web API را ویرایش نمی‌کنیم. اما این پورت را در پروژه‌ی کلاینت برای مثال به عدد 5002 تغییر می‌دهیم تا برنامه‌ی کلاینت، بر روی پورت پیش‌فرض برنامه‌ی Web API اجرا نشود:
{
    "BlazorWasm.Client": {
      "applicationUrl": "https://localhost:5002;http://localhost:5003",
    }  
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-27.zip
مطالب
بخش سوم - بررسی امکانات و کدنویسی در کامپایلر Svelte
در بخش‌های قبل تا حدودی با کامپایلر Svelte و ساختار فایل‌های آن آشنا شدیم. در این بخش با چند مثال قصد دارم امکاناتی را که Svelte در اختیارمان قرار میدهد، شرح دهم. 

Dynamic attributes : 
در بخش قبل دیدیم که با استفاده از علامت ( آکولاد {} ) میتوانیم مقادیر موجود در تگ اسکریپت را در html خود رندر کنیم. ولی از این علامت میتوان برای مقدار دهی attribute‌ها هم استفاده کرد. 
<script>
let src = 'https://svelte.dev/tutorial/image.gif';
let name = 'Rick Astley';
</script>

<img src={src} alt="{name} dancing">
اگر این کد را در بدنه کامپوننت خود یعنی همان فایل App.svelte قرار دهیم، به درستی کار خواهد کرد و مقدار‌های متغیر‌های src و name، در تگ img ما قرار خواهند گرفت. چند نکته در اینجا وجود دارد که بهتر است به آنها اشاره کنم.

  • نکته اول : اگر در تگ img مقدار alt را وارد نکنیم و یا alt در این تگ وجود نداشته باشد، یک هشدار توسط کامپایلر svelte برای ما با عنوان <img> element should have an alt attribute> ایجاد میشود. زمان ساخت یک برنامه بسیار مهم است تا قوانین نوشتن یک کد html خوب را رعایت کنیم تا برای تمامی کاربران احتمالی برنامه قابل استفاده باشد. در همین مثال با ایجاد یک هشدار Svelte تلاش میکند که ما را از اشتباه در نوشتن کد html مطلع سازد.

  • نکته دوم : اگر نام یک آبجکت تعریف شده و یک attribute، برابر باشد میتوانیم از نسخه کوتاه شده یا Shorthand attributes در svelte استفاده کنیم. به طور مثال در مثال بالا میتوانیم از کد زیر در خط 6 استفاده کنیم.
<img {src} alt="{name} dancing">


Nested components :
همانطور که قبلا اشاره کرده بودم، همه‌چیز در svelte از کامپوننت‌ها تشکیل میشود. در یک برنامه واقعی درست نیست که تمامی کد برنامه را مستقیما در کامپوننت اولیه برنامه بنویسیم. به جای آن بهتر است هر بخش از وبسایت، کامپوننت مجزایی داشته باشد تا کدها و استایل‌ها و html ما نظم و خوانایی بیشتری پیدا کنند.
یک فایل دیگر را در کنار کامپوننت App.svelte که در بخش قبلی ایجاد کرده بودیم، به نام Nested.svelte ایجاد کنید و در آن کد زیر را قرار دهید.
<script>
  export let siteName = "dotnettips";
</script>

<p>this is a nested component for third tutorial on {siteName}</p>
در این مرحله ما یک کامپوننت جدید را ایجاد کردیم که برای نمایش آن باید به App.svelte اضافه شود. برای انجام اینکار محتوای App.svelte را به کد زیر تغییر دهید.
<script>
  import Nested from "./Nested.svelte";

  export let name;
</script>

<h1>Hello {name}!</h1>

<Nested siteName="dotnettips.info" />
که در نهایت چنین خروجی خواهیم داشت. 
 Hello world!

this is a nested component for third tutorial on dotnettips.info


در مثال بالا ما یک کامپوننت جدید را ایجاد کرده و از طریق دستور import به App.svelte اضافه کردیم. نکته‌ای که در اینجا وجود دارد، نحوه مقدار دهی props در کامپوننت‌ها است. اگر به خط 9 دقت کنیم، کامپوننت ما از طریق تگ جدیدی با نام (Nested) به بدنه html برنامه اضافه شده است که یک attribute به نام siteName دارد. siteName متغیر export شده در کامپوننت Nested.svelte است که در کامپوننت‌ها به این صورت مقدار دهی میشود. قبلا نحوه مقدار دهی این خصیصه‌ها را در فایل‌های جاوا اسکریپت مشاهده کرده بودیم. نکته دیگری که باید به آن دقت داشت این است که خصیصه siteName مقدار پیش فرض dotnettips را در Nested.svelte به خود اختصاص داده بود. به همین جهت اگر ما siteName را هنگام استفاده از کامپوننت مقدار دهی نکنیم، از مقدار پیش فرض خود استفاده خواهد کرد. ولی اینجا ما با مقدار دهی آن، siteName را به dotnettips.info تغییر داده‌ایم.

نکته مهم : دقت داشته باشید کامپوننت‌های شما همیشه باید با حروف بزرگ شروع شوند؛ به طور مثال در صورت نوشتن <nested/> محتوای کامپوننت نمایش داده نخواهد شد. svelte، از طریق زیر نظر گرفتن حروف کوچک و بزرگ در ابتدای تگ‌ها، بین تگ‌های html و کامپوننت‌ها تمایز قائل میشود.



Spread  props :

تا اینجا به صورت خلاصه با props یا خصیصه‌ها آشنا شده‌اید و دیدیم که با export کردن یک متغیر در یک کامپوننت، میتوانیم آن را هنگام استفاده مقدار دهی نماییم. برای اینکه تمرینی هم باشد با توجه به مطالبی که تاکنون گفته شده، پروژه‌ی جدیدی را ایجاد کنید و محتوای App.svelte را مانند کد زیر تغییر دهید.

<script>
import Info from './Info.svelte';

const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>

<Info name={pkg.name} version={pkg.version} speed={pkg.speed} website={pkg.website}/>

همانطور که در خط دوم کد می‌بینید، کامپوننتی به نام Info.svelte به این بخش اضافه شده‌است. این کامپوننت را با محتوای زیر ایجاد نمایید:

<script>
export let name;
export let version;
export let speed;
export let website;
</script>

<p>
The <code>{name}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>

اگر برنامه را اجرا کنید یک چنین خروجی را مشاهده خواهید کرد: 

 The svelte  package is  blazing  fast. Download version  3  from  npm  and  learn more here
در این مثال در کامپوننت info.svelte حدودا تعداد زیادی متغیر export شده داریم که دقیقا همنام با آنها در App.svelte یک آبجکت به نام pkg مقدار دهی شده‌است (خط 4-9). در این شرایط نیازی به نوشتن تک تک خصیصه‌ها مانند کاری که در خط 12 کردیم نیست. خیلی ساده میتوانیم از امکانات ES6 برای destruct کردن این آبجکت و ست کردن این خصیصه‌ها استفاده کنیم؛ به این صورت : 
<Info {...pkg}/>



مروری کوتاه بر مدیریت Event ها  در svelte:
قدرت اصلی svelte، واکنش پذیر بودن آن است. به این معنی که همیشه DOM را با وضعیت یا state برنامه هماهنگ نگه میدارد. یکی از مواردی که بطور مثال این امکان را به خوبی نمایش میدهد، event‌ها هستند. 
به مثال زیر توجه کنید:
<script>
  let count = 0;

  function handleClick() {
    count += 1;
  }
</script>

<p>Count : {count}</p>
<button on:click={handleClick}>
  Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
در مثال بالا در خط 10، یک button را ایجاد کردیم که به متد click آن یک function اختصاص داده شده که در خط 4 تعریف کرده بودیم. برای bind کردن یک event در svelte از on: استفاده میکنیم. البته دقت کنید که در خط 10، نام function بدون پرانتز نوشته شده‌است؛ چرا که ما قصد اجرای آن را نداریم و صرفا میخواهیم این فانکشن پس از کلیک شدن بر روی button اجرا شود. در خط 5 هم در بدنه فانکشن خود که پس از هر کلیک اجرا خواهد شد، یک واحد به متغیر count اضافه میکنیم. در خطوط 9 و 11 هم مقدار count را نمایش داده‌ایم. 
در بخش‌های بعد به جزئیات کار با event‌ها بیشتر میپردازیم.

واکنش پذیری یا Reactivity در svelte :
svelte به صورت خودکار DOM را پس از اینکه وضعیت کامپوننت یا آبجکت‌های شما تغییر کند، آپدیت میکند؛ ولی در این بین چندین حالت وجود دارند که به صورت خودکار svelte از تغییر وضعیت کامپوننت شما آگاه نمیشود. 
عملگر :$
به مثال زیر توجه کنید. 
<script>
let count = 0;
let doubled = count * 2;

function handleClick() {
count += 1;
}
</script>

<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

<p>{count} doubled is {doubled}</p>
در این مثال در خط سوم متغیر doubled تعریف شده که قرار است دو برابر count را در خود نگه دارد. اگر این کد را اجرا نمایید، متوجه خواهید شد که مقدار double پس از تغییر count، تغییری نخواهد کرد. چرا که بر خلاف سایر فریم ورک‌ها، مانند react برای بهبود performance برنامه، svelte از Virtual-DOM استفاده نمیکند و این امر سبب میشود که کل کدهای کامپوننت ما در هر بار تغییر، در وضعیت بخشی از آن، مجددا اجرا نشود و مستقیما بخش مورد نظر در DOM تغییر کند. این امر سبب شده از نظر performance در کامپوننت‌های پیچیده که به طور مثال از انیمیشن یا visualisation‌ها استفاده میکنند، کامپوننت‌های svelte چندین برابر پرسرعت‌تر از سایر فریم ورک‌های مشابه عمل کند. ولی اگر بخواهیم که مقدار doubled آپدیت شود چه کاری باید کرد؟ 
راهکار اول مقدار دهی doubled در function مرتبط است، چرا که svelte همیشه به عملگر مساوی = در کد جاوا اسکریپت نگاه میکند. ولی این راهکار در مثال بالا منطقی به نظر نمی‌آید چرا که وظیفه متد ما صرفا دو برابر کردن مقدار count است.
راهکار دوم استفاده از عملگر :$  قبل از تعریف متغیر میباشد که به نظر راهکار مناسب‌تری است. پس در کد بالا فقط لازم است خط سوم را به کد زیر تغییر دهیم تا برنامه بدرستی مقدار doubled را پس از تغییر count محاسبه کند. 
$: doubled = count * 2;
به این طریق میتوانیم متغیر خود را وادار کنیم درصورت تغییر مقادیر، در سمت راست مساوی، مقدار doubled را مجددا محاسبه نماید.

استفاده از این عملگر در svelte به تعریف متغیر‌ها محدود نمیشود. ما هر جائیکه نیاز به واکنش پذیری نسبت به کامپوننت‌ها یا متغیر‌های خود داشتیم، میتوانیم از این عملگر استفاده کنیم. به طور مثال برای لاگ کردن اطلاعات، هر بار که مقدار count تغییر کرد میتوانیم از کد زیر استفاده کنیم. 
$: console.log(`the count is ${count}`);
همچنین میتوانیم به آسانی گروهی از فعالیت‌ها را با هم واکنش پذیر کنیم. مثال :
$: {
  console.log(`the count is ${count}`);
  alert(`I SAID THE COUNT IS ${count}`);
}
حتی میتوان از این عملگر قبل از بلاک‌های کد، مانند if یا for استفاده کرد. مثال:
$: if (count >= 10) {
     alert(`count is dangerously high!`);
     count = 9;
}

واکنش پذیری در آرایه‌ها و آبجکت‌ها : 
به مثال زیر توجه کنید:
<script>
  let numbers = [1, 2, 3, 4];

  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers.push(newNumber);
  }

  $: sum = numbers.reduce((t, n) => t + n, 0);
</script>

<p>{numbers.join(' + ')} = {sum}</p>

<button on:click={addNumber}>Add a number</button>

در مثال بالا پس از هر کلیک، یک عدد به آرایه numbers اضافه میشود؛ ولی در خط 11 که این آرایه را نمایش میدهیم، مقدار آن تغییر نخواهد کرد! همانطور که قبلا اشاره کرده بودم svelte برای اینکه متوجه شود تغییری در آبجکت‌های ما صورت گرفته است، به مشاهده عملگر مساوی در کدهای ما میپردازد و چون اینجا در خط 6، ما از متد push آرایه برای افزودن مقداری جدید به آن درحال استفاده هستیم svelte واکنشی به این تغییر ندارد. برای رفع این مشکل دو راهکار وجود دارد. 
راهکار اول استفاده از عملگر مساوی و reassign کردن مقدار آبجکت یا متغیر مورد نظر است به این صورت : 
  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers.push(newNumber);
    numbers = numbers;
  }
راهکار دوم استفاده از امکانات ES6 جاوا اسکریپت است برای افزودن مقدار جدید به آرایه که چون در این روش از مساوی استفاده میکنیم، تغییرات در numbers واکنش پذیر خواهد بود. به این صورت : 
  function addNumber() {
    let newNumber = numbers.length + 1;
    numbers = [...numbers, newNumber];
  }

مروری بر Two way bindings :
فرض کنید قصد داریم که یک input برای دریافت نام در صفحه قرار داده و در متغیری به نام name ذخیره کنیم. سپس این متغیر را در صفحه نشان دهیم. برای انجام اینکار حدودا با توجه به مطالبی که تا الان آموخته‌ایم میتوان کد زیر را تولید کرد : 
<script>
  let name = "";
  function updateName(event) {
    name = event.target.value;
  }
</script>

<h4>My Name Is {name}</h4>
<input value={name} on:input={updateName} />
در خط 9 یک input به کامپوننت خود اضافه کردیم و مقدار value آن را به متغیر name نسبت دادیم. تا اینجا فقط اگر name تغییری کند آن را در input میتوانیم نمایش دهیم ولی پس از تایپ در این input به صورت خودکار مقدار name آپدیت نمیشود چون ما در این حالت از One way binding درحال استفاده هستیم. به همین جهت یک متد برای آپدیت مقدار name توسط input هم باید مینوشتیم که اگر دقت کنید در کد فوق از ایونت input تگ input در خط 11 به همین منظور استفاده شده‌است. در خط سوم اینبار متد ما یک پارامتر هم به نام event دارد. به طور پیش فرض میتوانیم همیشه از طریق این پارامتر به ایونت آبجکت مبداء که فانکشن را صدا زده است، دسترسی داشته باشیم که خیلی ساده از طریق این ایونت در خط 4 مقدار name را پس تغییر در input آپدیت میکنیم. 
شاید کد بالا کاری که خواستیم را انجام داده باشد. ولی پیاده سازی Two way binding به شکلی که نوشتیم ساده نیست و مشکلات خاص خودش را دارد. خبر خوب این است که svelte این امکان را به صورت توکار محیا کرده است. 
برای اتصال یک attribute به یک متغیر یا آبجکت به صورت دو طرفه از کلمه :bind قبل از نام attribute در svelte استفاده میشود. مثال بالا را اگر با استفاده از این توضیح بازنویسی کنیم، به این کد خواهیم رسید:
<script>
  let name = "";
</script>

<h4>My Name Is {name}</h4>
<input bind:value={name} />
همانطور که مشاهده میکنید دیگر نیازی به نوشتن یک فانکشن مجزا و گرفتن اطلاعات از طریق پارامتر ایونت نیست و به سادگی مقدار value به متغیر name به صورت دو طرفه متصل شده است.


expressing logic :
html امکانی برای پیاده سازی منطق برنامه، برخلاف svelte ندارد. در ادامه با syntax پیاده سازی منطق برنامه در کنار کدهای html در svelte آشنا میشویم. 

If blocks
برای نوشتن If در svelte از if# در میان دو آکولاد {} استفاده میشود و برای پایان دادن به آن از if/ در میاد دو آکولاد. به این صورت : 
{#if condition}
    <!-- you html codes ...  -->
{/if}
به مثال زیر توجه کنید : 
در این مثال قصد داریم پس از کلیک بر روی کلید لاگین، وضعیت کاربر را لاگین شده قرار دهیم و کلید لاگین را مخفی کنیم. سپس کلید دیگری برای خروج از برنامه به نام logout را نمایش دهیم که با کلیک بر روی آن کاربر logout شود و مجددا کلید لاگین نمایش داده شود. برای شبیه سازی این سناریو از کد زیر میتوانیم استفاده کنیم :
<script>
let user = { loggedIn: false };

function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>

{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}

{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
کدهای داخل اسکریپت که چندان نیازی به توضیح ندارند و صرفا برای toggle کردن وضعیت کاربر هستند. ولی در خطوط 9 - 13 و 15 - 19 طریقه نوشتن if در svelte را با یک مثال واقعی مشاهده میکنید.
در مثال بالا دو شرط با یک مقدار مشابه true/false نوشته شده است که بهتر بود مانند سایر زبان‌های برنامه نویسی از else در اینجا استفاده میشد. در ادامه با نحوه نوشتن else در svelte آشنا خواهیم شد.
Else blocks
برای نوشتن else در svelte از else: در میاد دو آکولاد {} بین یک block مانند if میتوانیم استفاده کنیم. به این صورت :
{#if condition}
    <!-- you html code when condition is true -->
{:else}
    <!-- you html code when condition is false -->
{/if}
باز نویسی دو if block در مثال بالا با استفاده از else : 
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}

همینطور شما میتوانید از else if هم استفاده نمایید. به این صورت : 
{#if condition}
    <!-- you html code when condition is true -->
{:else if condition2}
    <!-- you html code when condition2 is true -->
{:else}
    <!-- you html code when condition and condition2 are false -->
{/if}

نکته تکمیلی : در svelte علامت # همیشه به معنای شروع یک بلاک کد است و علامت / به معنای پایان بلاک. همینطور علامت : نشان دهنده ادامه بلاک کد است. شاید در ابتدا کمی گیج کننده به نظر بیاید ولی در مثال‌های بعدی بیشتر با این علائم آشنا میشویم.

each blocks : 
برای حرکت در میان لیستی از اطلاعات در svelte میتوانیم از each# استفاده نماییم و مانند بلاک if برای خاتمه دادن به آن از each/ استفاده میشود. همینطور از کلمه کلیدی as هم برای دسترسی به هر آیتم در لیست استفاده میشود. به این صورت : 
{#each list as item}
      <!-- you html code per each item in list -->
{/each}
به مثال زیر توجه کنید : 
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>

<h1>The Famous Cats of YouTube</h1>

<ul>
{#each cats as cat}
<li><a target="_blank" href="https://www.youtube.com/watch?v={cat.id}">
{cat.name}
</a></li>
{/each}
</ul>
در خط 2 ما یک آرایه از گربه‌ها را تعریف کرده ایم و در خط 11 با استفاده از each به ازای هر کدام از آیتم‌های داخل این آرایه یک تگ li ایجاد میکنیم که لینک مرتبط با آن آیتم را نمایش میدهد. البته کد فوق را به شکل دیگری با استفاده از امکانات destructing جاوا اسکریپت هم میتوانستیم بنویسیم. به این صورت : 
<ul>
{#each cats as {id,name}}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{name}
</a></li>
{/each}
</ul>
در این حالت ابتدا id و name به صورت مجزا destruct شده اند و میتوانیم مانند مثال بالا از آنها بدون نوشتن نام آیتم استفاده کنیم . مانند خط 4 کد بالا.
همینطور در each بلاک‌ها امکان دریافت ایندکس جاری آیتم از طریق آرگومان دوم هم وجود دارد. به این صورت : 
<ul>
{#each cats as { id, name }, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a></li>
{/each}
</ul>
اگر به کد فوق دقت کنید درخط 2 مقدار i ایندکس آیتم جاری آرایه ما یعنی همان cats است.


نکته : در این بخش من سعی کردم تا حدودی به ترتیب بخش آموزشی خود وبسایت Svelte، موارد را بیان کنم؛ ولی با توجه به اینکه شاید دوستان ترجیح بدهند روش آموزشی خود  آن وبسایت که امکان تغییر و نوشتن کد را هم محیا کرده است، امتحان کنند  لینک آن  را به اشتراک میگذارم.