نظرات مطالب
ساختار پروژه های 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){
...
}]);


مطالب
بررسی روش تعریف انقیاد دو طرفه‌ی سفارشی در کامپوننت‌های Angular
برخلاف AngularJS، در برنامه‌های Angular امکانات two way data binding به صورت پیش‌فرض ارائه نمی‌شوند تا از تمام مشکلات آن مانند digest cycle ،watchers و غیره خبری نباشد. اما گاهی از اوقات نیاز است انقیاد دو طرفه‌ی سفارشی را بین دو کامپوننت ایجاد کنیم. در این مطلب روش ایجاد یک چنین انقیادهایی را بررسی خواهیم کرد و در اینجا در ابتدا نیاز است دو پیشنیاز Property Binding و Event Binding را بررسی کنیم که از جمع آن‌ها two way data binding حاصل می‌شود:


البته Angular به همراه دایرکتیو ویژه‌ای به نام ngModel است که two-way data binding را با import ماژول ویژه‌ی فرم‌ها میسر می‌کند:


که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شده‌است:
<input [ngModel]="username" (ngModelChange)="username = $event">
و یا به صورت خلاصه:
<input [(ngModel)]='username' />
در اینجا می‌خواهیم یک چنین امکانی را بدون استفاده از ngModel و ماژول فرم‌ها پیاده سازی کنیم.


انقیاد به خواص یا Property binding

فرض کنید دو کامپوننت والد و فرزند را ایجاد کرده‌ایم:


در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش می‌دهیم:
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-parent",
  templateUrl: "./parent.component.html",
  styleUrls: ["./parent.component.css"]
})
export class ParentComponent implements OnInit {

  amount = 500;

  constructor() { }

  ngOnInit() {
  }

  deposit() {
    this.amount += 100;
  }
}
با این قالب:
<h2>Custom two way data binding</h2>

<div class="panel panel-primary">
  <div class="panel-heading">
    <h2 class="panel-title">Parnet Component</h2>
  </div>
  <div class="panel-body">
    <label>Available amount:</label> {{amount}}
    <button (click)="deposit()" class="btn btn-success">Deposit 100</button>
    <div>
      <app-child [amount]="amount"> </app-child>
    </div>
  </div>
</div>
که در آن مقدار amount کامپوننت والد نمایش داده شده‌است و همچنین این مقدار به خاصیت ورودی کامپوننتی به نام app-child نیز نسبت داده شده‌است.

کامپوننت فرزند به صورت ذیل تعریف می‌شود:
import { Component, OnInit, Input } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
  }
}
که در آن خاصیت amount، از والد آن، توسط ویژگی Input دریافت می‌شود. سپس در متد withdraw هربار می‌توان 100 آیتم را از آن کسر کرد.
با این قالب:
<div class="panel panel-default">
  <div class="panel-heading">
    <h2 class="panel-title">Child Component</h2>
  </div>
  <div class="panel-body">
    <label>Amount available: </label> {{amount}}

    <button (click)="withdraw()" class="btn btn-danger">Withdraw 100</button>
  </div>
</div>
که در آن مقدار amount فرزند نمایش داده شده‌است و همچنین امکان فراخوانی متد withdraw وجود دارد.

در اینجا زمانیکه data binding را به صورت ذیل تعریف می‌کنیم:
<app-child [amount]="amount"> </app-child>
روش مقدار دهی خاصیت amount داخل [] ، انقیاد به خواص نامیده می‌شود و سمت راست آن نیز یک خاصیت درنظر گرفته می‌شود. یعنی مقدار خاصیت amount والد (درون "") به مقدار خاصیت amount فرزند (درون []) نسبت داده خواهد شد.
این ارتباط نیز یک طرفه‌است. برای مثال اگر بر روی دکمه‌ی Deposit والد کلیک کنیم:


مقدار افزایش یافته‌ی در والد، به فرزند نیز منتقل می‌شود و نمایش داده خواهد شد. اما اگر بر روی دکمه‌ی withdraw فرزند کلیک کنیم:


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


انقیاد به رخ‌دادها یا Event binding

یک کامپوننت می‌تواند به رخ‌دادهای صادر شده‌ی توسط کامپوننتی دیگر گوش فرا دهد:
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "app-child",
  templateUrl: "./child.component.html",
  styleUrls: ["./child.component.css"]
})
export class ChildComponent implements OnInit {

  @Input() amount: number;
  @Output() amountChange = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

  withdraw() {
    this.amount -= 100;
    this.amountChange.emit(this.amount);
  }
}
برای این منظور در کامپوننت فرزند، یک خاصیت Output را به نام amountChange از نوع EventEmitter تعریف می‌کنیم. سپس جایی که قرار است کار کاهش amount صورت گیرد، با صدور رخ‌دادی (this.amountChange.emit)، این مقدار را به والد اعلام می‌کنیم.
اکنون در قالب کامپوننت والد، این رخ‌داد را درون یک () معرفی خواهیم کرد:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به این ترتیب زمانیکه کامپوننت فرزند، مقدار amount را تغییر می‌دهد، این مقدار توسط this.amountChange.emit به والد منتشر خواهد شد و می‌توان در سمت والد توسط event$ به آن دسترسی یافته و آن‌را به خاصیت this.amount کامپوننت والد نسبت دهیم.
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمه‌ی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس می‌شود:



پیاده سازی syntax ویژه‌ی Banana in a box

تا اینجا پیاده سازی two way data-binding سفارشی به پایان می‌رسد. اما تعریف طولانی:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
به صورت ذیل هم قابل نوشتن و ساده سازی است:
<app-child [(amount)]="amount"> </app-child>
که به آن syntax ویژه Banana in a box نیز گفته می‌شود.
نکته‌ی ویژه‌ی آن، وجود پسوند Change در نام رخ‌داد تعریف شده‌است:
  @Input() amount: number;
  @Output() amountChange = new EventEmitter();
 اگر نام خاصیت Input مساوی x باشد، باید جهت فعالسازی syntax ویژه Banana in a box، نام رخ‌داد متناظر با آن دقیقا مساوی xChange انتخاب شود. مانند amount ورودی در اینجا و amountChange خروجی تعریف شده.

بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخ‌داد خروجی هم‌نام، که نام آن، پسوند Change را اضافه‌تر دارد، یعنی xChange را تعریف کرد.
- اکنون می‌توان two-way data binding syntax ویژه‌ای را به نام banana in a box بر روی این‌دو تعریف کرد[(x)].


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
کامپوننت‌ها در AngularJS 1.5 - قسمت دوم - مسیریابی
در این قسمت به معرفی سیستم مسیریاب در Angular 1.5 خواهیم پرداخت. قبل از معرفی این سیستم ابتدا سیستم مسیریاب اصلی در Angular را بررسی خواهیم کرد.

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

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

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

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

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

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


معرفی Component Router

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

bower install angular-component-router --save

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

مسیرراه‌ها
ASP.NET MVC
              مطالب
              بارگذاری پویای کامپوننت‌های Angular به همراه امکان Lazy loading پویای ماژول‌ها

              در نسخه‌های قبل از Angular CLI 6.0، صرفا امکان Bundle کردن جداگانه‌ی ماژول‌هایی که در قسمت  loadChildren مرتبط با تنظیمات مسیریابی  ذکر شده بودند، وجود داشت. بنابراین در برخی از شرایط اگر نیاز به امکان بارگذاری ماژولی به صورت Lazy load بود، باید از سیستم مسیریابی استفاده می‌شد یا اینکه با یکسری ترفند، CLI و Webpack را مجبور به ساخت فایل chunk جداگانه برای ماژول مورد نظر می‌کردید. از زمان انتشار Angular CLI 6.0 امکان Lazy loading پویا نیز مهیا می‌باشد؛ به این ترتیب بدون وابستگی به سیستم مسیریابی، باز هم می‌توان از مزایای Lazy loading بهره برد. در این مطلب روش استفاده از این قابلیت و همچنین نحوه‌ی بارگذاری پویای یک کامپوننت مرتبط با یک ماژول Lazy load شده را بررسی خواهیم کرد. برای این منظور در ادامه با ایجاد یک TabLayout با استفاده از Angular Material Tabs با یکی از موارد پر استفاده‌ی قابلیت مذکور آشنا خواهیم شد.

              پیش نیازها

              کار را با طراحی و پیاده سازی TabService شروع می‌کنیم. برای این منظور یک سرویس را در فولدر services موجود در کنار CoreModule ایجاد خواهیم کرد؛ به این جهت ابتدا مدل‌های زیر را خواهیم داشت:

              import { Type, ValueProvider } from '@angular/core';
              
              export interface OpenNewTabModel {
                label: string;
                componentType: Type<any>;
                iconName: string;
                modulePath?: string;
                data?: ValueProvider[];
              }
              واسط تعریف شده‌ی در بالا به عنوان قرارداد مدل ورودی متد open مرتبط با سرویس TabService استفاده می‌شود. در اینجا componentType، نوع کامپوننت را مشخص می‌کند که قرار است داخل برگه‌ی جدید نمایش داده شود. modulePath هم به مسیر ماژولی که باید به صورت پویا بارگذاری شود، اشاره می‌کند. دلیل وجود خصوصیت data را نیز در ادامه خواهیم دید.
              import { TabItemComponent } from './tab-item-component';
              
              export interface TabItem {
                label: string;
                iconName: string;
                component: TabItemComponent;
              }

              OpenNewTabModel برای ارسال داده توسط مصرف کننده از این سرویس در نظر گرفته شده است. ولی واسط TabItem دارای خصوصیاتی می‌باشد که ما برای نمایش یک برگه‌ی جدید نیازمندیم. TabItemComponent نیز دارای خصوصیاتی است که مورد نیاز دایرکتیو« NgComponentOutlet» است. 

              import { Injector, NgModuleFactory, Type } from '@angular/core';
              
              export interface TabItemComponent {
                componentType: Type<any>;
                moduleFactory?: NgModuleFactory<any>;
                injector: Injector;
              }

              همانطور که اشاره شد، برای بارگذاری پویای یک کامپوننت از NgComponentOutlet استفاده خواهیم کرد؛ لذا اگر modulePath ای توسط مصرف کننده از TabService، مهیا شده باشد، لازم است ابتدا ماژول مورد نظر به صورت پویا بارگذاری شود و moduleFactory بدست آمده را به عنوان ورودی دایرکتیو مذکور ارسال کنیم. TabService، پیاده سازی به شکل زیر خواهد داشت:
              import { BehaviorSubject, Observable } from 'rxjs';
              import {
                Injectable,
                Injector,
                NgModuleFactory,
                NgModuleFactoryLoader
              } from '@angular/core';
              
              import { OpenNewTabModel } from '../models/open-new-tab-model';
              import { TabItem } from '../models/tab-item';
              
              @Injectable({
                providedIn: 'root'
              })
              export class TabService {
                private tabItemSubject: BehaviorSubject<TabItem[]> = new BehaviorSubject<
                  TabItem[]
                >([]);
              
                constructor(
                  private loader: NgModuleFactoryLoader,
                  private injector: Injector
                ) {}
              
                get tabItems$(): Observable<TabItem[]> {
                  return this.tabItemSubject.asObservable();
                }
              
                open(newTab: OpenNewTabModel) {
                  if (newTab.modulePath) {
                    this.loader
                      .load(newTab.modulePath)
                      .then((moduleFactory: NgModuleFactory<any>) => {
                        this.openInternal(newTab, moduleFactory);
                      });
                  } else {
                    this.openInternal(newTab);
                  }
                }
              
                private openInternal(newTab: OpenNewTabModel, moduleFactory?: NgModuleFactory<any>) {
                  const newTabItem: TabItem = {
                    label: newTab.label,
                    iconName: newTab.iconName,
                    component: {
                      componentType: newTab.componentType,
                      moduleFactory: moduleFactory,
                      injector: newTab.data
                        ? Injector.create(newTab.data, this.injector)
                        : this.injector
                    }
                  };
              
                  this.tabItemSubject.getValue().push(newTabItem);
                  this.tabItemSubject.next(this.tabItemSubject.getValue());
                }
              
                close(index: number) {
                  this.tabItemSubject.getValue().splice(index, 1);
                  this.tabItemSubject.next(this.tabItemSubject.getValue());
                }
              }
              روش کار به این شکل می‌باشد که یک مخزن، برای لیست برگه‌های درخواستی برای نمایش، تحت عنوان tabItemSubject و از نوع BehaviorSubject در نظر گرفته شده تا مصرف کننده از این سرویس که قصد نمایش برگه‌ها را دارد، از تغییرات لیست موجود آگاه شود. عموما TabsComponent، مشترک پراپرتی فقط خواندنی ‎‎‎tabItems‎$ خواهد شد و بقیه بخش‌ها صرفا دستور گشودن برگه‌ی جدید را با متد open صادر خواهند کرد.
              یکی از وابستگی‌های این سرویس، وهله‌ای می‌باشد از کلاس  NgModuleFactoryLoader  که در سیستم مسیریابی نیز از همین کلاس برای بارگذاری ماژول‌ها استفاده می‌شود. البته نیاز است که یکی از پیاده سازی‌های این کلاس انتزاعی را به سیستم تزریق وابستگی‌ها نیز معرفی کنید:
              { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
              در بدنه متد open، ابتدا بررسی می‌شود که اگر modulePath مشخص شده‌است، ماژول مورد نظر ابتدا توسط متد load مرتبط با وهله NgModuleFactoryLoader به صورت پویا بارگذاری شود و سپس با استفاده از moduleFactory بدست آمده، متد openInternal فراخوانی خواهد شد.
               در بدنه متد openInternal، تنهای نکته‌ای که ذکر آن اهمیت دارد، مرتبط است به مقداردهی خصوصیت injector شیء ایجاد شده. باتوجه به اینکه تا زمان نگارش مطلب جاری امکان کار با Input‌ها و Output‌های کامپوننت مورد نظر که قرار است با استفاده از NgComponentOutlet بارگذاری شود، وجود ندارد، لذا راه حل فعلی، استفاده از سیستم تزریق وابستگی‌ها می‌باشد. برای این منظور، با استفاده از متد استاتیک create کلاس Injector یک child injector ایجاد شده و ValueProvider‌های مشخص شده توسط خصوصیت data، به صورت خودکار رجیستر خواهند شد. در نهایت آگاه سازی مشترکین خصوصیت ‎‎‎tabItems‎با استفاده از فراخوانی متد next مرتبط با tabItemSubject انجام می‌گیرد.

              پیاده سازی TabsComponent
              import { Component, OnInit } from '@angular/core';
              
              import { TabService } from './../../../services/tab.service';
              
              @Component({
                selector: 'app-tabs',
                templateUrl: './tabs.component.html',
                styleUrls: ['./tabs.component.scss']
              })
              export class TabsComponent implements OnInit {
                constructor(public service: TabService) {}
              
                ngOnInit() {}
              }

              همانطور که عنوان شد، مشترک اصلی خصوصیت tabItems سرویس TabService، کامپوننت تعریف شده‌ی بالا می‌باشد. قالب مرتبط با آن به شکل زیر است:
              <mat-tab-group>
                <mat-tab
                  *ngFor="let tabItem of (service.tabItems$ | async); let index = index"
                >
                  <ng-template mat-tab-label>
                    <mat-icon
                      class="icon"
                      aria-label="icon for tab"
                    >{{tabItem.iconName}}</mat-icon>
                    <span class="full">{{ tabItem.label }}</span>
                  
                    <mat-icon
                      class="close"
                      (click)="service.close(index)"
                      aria-label="close tab button"
                      >close</mat-icon
                    >
                    <!-- </button> -->
                  </ng-template>
              
                  <ng-container *ngIf="tabItem.component.moduleFactory">
                    <ng-container
                      *ngComponentOutlet="
                        tabItem.component.componentType;
                        ngModuleFactory: tabItem.component.moduleFactory;
                        injector: tabItem.component.injector
                      "
                    >
                    </ng-container>
                  </ng-container>
                  <ng-container *ngIf="!tabItem.component.moduleFactory">
                    <ng-container
                      *ngComponentOutlet="
                        tabItem.component.componentType;
                        injector: tabItem.component.injector
                      "
                    >
                    </ng-container>
                  </ng-container>
                </mat-tab>
              </mat-tab-group>

              در تکه کد بالا، ابتدا با استفاده از وهله تزریق شده TabService در کامپوننت مذکور، به شکل زیر، مشترک تغییرات لیست برگه‌ها شده‌ایم و با استفاده از دایرکتیو ‎*ngFor به ازای تک تک tabItem‌های درخواست شده برای گشوده شدن، به شکل زیر کار وهله سازی پویا از کامپوننت مشخص شده انجام می‌شود:

              <ng-container *ngComponentOutlet="tabItem.component.componentType; ngModuleFactory: tabItem.component.moduleFactory; injector: tabItem.component.injector">
              </ng-container>

              خوب، با استفاده از آنچه تا اینجای مطلب بررسی شد، می‌توان یک سیستم راهبری مبتنی بر Tab را نیز برپا کرد که مطلب جدایی را می‌طلبد. برای تکمیل مکانیزم بارگذاری پویای ماژول‌ها، نیاز است تا مسیر ماژول مورد نظر را در فایل angular.json و بخش lazyModules به شکل زیر معرفی کنید:

              "build": {
                        "builder": "@angular-devkit/build-angular:browser",
                        "options": {
                          "outputPath": "dist/MaterialAngularTabLayout",
                          "index": "src/index.html",
                          "main": "src/main.ts",
                          "polyfills": "src/polyfills.ts",
                          "tsConfig": "src/tsconfig.app.json",
                          "assets": [
                            "src/favicon.ico",
                            "src/assets"
                          ],
                          "styles": [
                            "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
                            "src/styles.scss"
                          ],
                          "lazyModules": [
                            "src/app/lazy/lazy.module"
                          ],
                          "scripts": []
                        },

              به عنوان مثال قصد داریم ماژول LazyModule را به صورت پویا بارگذاری کرده و LazyComponent موجود در این ماژول را به صورت پویا در برگه‌ی جدیدی نمایش دهیم. برای این منظور کدهای فایل AppComponent.ts را به شکل زیر تغییر خواهیم داد:

              import { Component } from '@angular/core';
              import { IdModel } from './core/models/id-model';
              import { LazyComponent } from './lazy/lazy.component';
              import { OpenNewTabModel } from './core/models/open-new-tab-model';
              import { TabService } from './core/services/tab.service';
              
              @Component({
                selector: 'app-root',
                templateUrl: './app.component.html',
                styleUrls: ['./app.component.scss']
              })
              export class AppComponent {
                title = 'MaterialAngularTabLayout';
                constructor(private tabService: TabService) {}
                loadLazyComponent() {
                  this.tabService.open(<OpenNewTabModel>{
                    label: 'Loaded Lazy Component',
                    iconName: 'thumb_up',
                    componentType: LazyComponent,
                    modulePath: 'src/app/lazy/lazy.module#LazyModule',
                    data: [{ provide: IdModel, useValue: <IdModel>{ id: 1 } }]
                  });
                }
              }

              در تکه کد بالا با تزریق TabService به سازنده‌ی آن، قصد گشودن برگه‌ی جدیدی را توسط متد open آن، داریم. در بدنه‌ی متد loadLazyComponent یک شیء با قرارداد OpenNewTabModel ایجاد شده و به عنوان آرگومان به متد open ارسال شده است. توجه داشته باشید که modulePath اینجا نیز به مانند خصوصیت loadChildren مرتبط با اشیاء مسیریابی، باید مقدار دهی شود. همچنین قصد داشتیم اطلاعاتی را نیز به کامپوننت مورد نظر ارسال کنیم؛ همانند مکانیزم مسیریابی که با پارامترها این چنین کارهایی صورت می‌پذیرد. در اینجا از یک کلاس به شکل زیر استفاده شده‌است:

              export class IdModel {
                constructor(public id: number) {}
              }

              در این صورت پیاده سازی LazyComponent نیز به شکل زیر خواهد بود:

              import { Component, OnInit } from '@angular/core';
              
              import { IdModel } from './../core/models/id-model';
              
              @Component({
                selector: 'app-lazy',
                templateUrl: './lazy.component.html',
                styleUrls: ['./lazy.component.scss']
              })
              export class LazyComponent implements OnInit {
                constructor(private model: IdModel) {}
              
                ngOnInit() {
                  console.log(this.model);
                }
              }

              البته فراموش نکنید کامپوننتی را که نیاز است به صورت پویا بارگذاری شود، در قسمت entryComponents مرتبط با NgModule متناظر به شکل نیز معرفی کنید:

              import { CommonModule } from '@angular/common';
              import { LazyComponent } from './lazy.component';
              import { NgModule } from '@angular/core';
              
              @NgModule({
                imports: [CommonModule],
                declarations: [LazyComponent],
                entryComponents: [LazyComponent]
              })
              export class LazyModule {}

              با خروجی زیر:

              و chunk تولید شده برای ماژول مورد نظر:


              در صورتیکه در حالت production پروژه را بیلد کنید، هش مرتبط برای chunk تولید شده نیز ایجاد خواهد شد.


              کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید.
              مطالب
              شروع به کار با AngularJS 2.0 و TypeScript - قسمت ششم - کامپوننت‌های تو در تو
              گاهی از اوقات جهت refactoring یک template بزرگ، بهتر است آن‌را به چند template کوچک خرد کرد و سپس از جمع آن‌ها به صورت یک template اصلی استفاده نمود. در این حالت نیاز است بین این زیر کامپوننت‌ها و کامپوننت‌های دربرگیرنده‌ی آن‌ها ارتباطات لازم را برقرار کرد.
              تا اینجا در قسمت سوم، نحوه‌ی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کننده‌ی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننت‌ها ارتباط برقرار کرد؟


              تهیه کامپوننت نمایش ستاره‌ای امتیازهای محصولات

              مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت می‌خواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستاره‌ای آن‌ها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرنده‌ی آن دریافت کند. همچنین می‌خواهیم اگر کاربر بر روی این ستاره‌ها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
              در این مثال در فایل product-list.component.html چنین سطری تعریف شده‌است:
               <td>{{ product.starRating }}</td>
              البته می‌توان در همینجا کدهای نمایش ستاره‌ای را بجای درج مقدار عددی آن قرار داد، اما ... قالب جاری را بیش از اندازه شلوغ خواهد کرد. به همین دلیل بهتر است نمایش آن‌را تبدیل به یک کامپوننت مجزا کرد. به علاوه در این حالت، قابلیت استفاده‌ی مجدد از آن در سایر کامپوننت‌ها نیز وجود خواهد داشت.
              با توجه به اینکه کامپوننت نمایش ستاره‌ای امتیازها، قابلیت استفاده‌ی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آن‌را به خارج از پوشه‌ی products فعلی منتقل کنیم. برای مثال می‌توان پوشه‌ی app\shared را برای آن و تمامی کامپوننت‌های با قابلیت استفاده‌ی مجدد ایجاد کرد.


              برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
              import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core';
               
              @Component({
                  selector: 'ai-star',
                  templateUrl: 'app/shared/star.component.html',
                  styleUrls: ['app/shared/star.component.css']
              })
              export class StarComponent implements OnChanges {
                  @Input() rating: number;
                  starWidth: number;
                  @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
               
                  ngOnChanges(): void {
                      this.starWidth = this.rating * 86 / 5;
                  }
               
                  onClick() {
                      this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
                  }
              }
              روش ساخت این کامپوننت نیز همانند سایر کامپوننت‌ها است و اصول کلی آن تفاوتی نمی‌کند. در اینجا نیز کلاسی وجود دارد که export شده و همچنین به Component مزین است. مقدار selector آن نیز به ai-star تنظیم شده‌است.
              سپس مسیر template و مسیر فایل css ویژه‌ی آن، در تزئین کننده‌ی Component مشخص شده‌اند. محتوای کامل این دو فایل را در ذیل مشاهده می‌کنید:
              الف) محتوای فایل App\shared\star.component.html
              <div class="crop"
                   [style.width.px]="starWidth"
                   [title]="rating"
                   (click)='onClick()'>
                  <div style="width: 86px">
                      <span class="glyphicon glyphicon-star"></span>
                      <span class="glyphicon glyphicon-star"></span>
                      <span class="glyphicon glyphicon-star"></span>
                      <span class="glyphicon glyphicon-star"></span>
                      <span class="glyphicon glyphicon-star"></span>
                  </div>
              </div>
              ب) محتوای فایل App\shared\star.component.css
              .crop {
                  overflow: hidden;
              }
               
              div {
                  cursor: pointer;
              }
              قالب star.component.html کار نمایش پنج ستاره را انجام می‌دهد. عرض کلی آن بر اساس مقدار خاصیت starWidth مشخص می‌شود و بر همین اساس، تعداد نمایان ستاره‌ها، مشخص خواهند شد. خاصیت starWidth به width این div بر حسب px، متصل شده‌است (property binding). همچنین خاصیت title این div نیز به مقدار rating متصل شده‌است و اگر بر روی آن کلیک شود، متد onClick را در کلاس متناظر با کامپوننت خود، فراخوانی خواهد کرد (event binding).

              معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده می‌شود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه می‌شود.
              کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستاره‌ها است.


              مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد

              متد ngOnChanges، تنها به خواص ویژه‌ای به نام «input properties» واکنش نشان می‌دهد. اگر یک کامپوننت تو در توی قرار گرفته‌ی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژه‌ای به نام ()Input@ انجام می‌شود.
              به همین جهت است که پیش از خاصیت rating در کلاس  StarComponent، شاهد درج مزین کننده‌ی ویژه‌ی ()Input@ هستیم:
              export class StarComponent implements OnChanges {
                    @Input() rating: number;
              مزین کننده‌ی ()Input@ را به هر خاصیتی با هر نوعی، می‌توان انتساب داد. در اینجا نیاز است کامپوننت نمایش ستاره‌ای امتیازها، عدد امتیاز را از والد خود دریافت کنند. به همین جهت خاصیت امتیاز را از نوع «خاصیت ورودی» مشخص کرده‌ایم.
              پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی می‌کند:
               [rating]='product.starRating'

              بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشده‌است، قابلیت property binging فوق را در کامپوننت والد، ندارد.

              اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که می‌توان از این مقدار تغییر یافته، جهت ترجمه‌ی آن به عرض div نمایش ستاره‌ها، استفاده کرد.


              ارسال داده‌ها از کامپوننت فرزند به کامپوننت والد

              تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
              کامپوننت فرزند، با استفاده از decorator ویژه‌ی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا می‌کند:
              export class StarComponent implements OnChanges {
                  @Input() rating: number;
                  starWidth: number;
                  @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
              در اینجا خاصیت ratingClicked را که با ()Output@ مزین شده‌است، مشاهده می‌کنید. نوع این خاصیت باید از نوع رخدادها باشد که در AngularJS 2.0 توسط شیء EventEmitter جنریک، تعریف شده‌است. در این مثال، نوع رخداد و مقداری که توسط آن ارسال می‌شود، رشته‌ای درنظر گرفته شده‌است. اگر نیاز به ارسال چندین مقدار بود، می‌توان یک شیء را در اینجا مشخص کرد.
              در مثال جاری اگر کاربر بر روی div ستاره‌های نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام می‌شود (متد جدید onClick به رخداد click متصل شده‌است):
              <div class="crop"
                   [style.width.px]="starWidth"
                   [title]="rating"
                   (click)='onClick()'>
              و سپس این رخداد کلیک، در کلاس StarComponent به نحو ذیل به والد خود منتقل خواهد شد:
              onClick() {
                   this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
               }
              در اینجا متد emit است که اطلاعات را به کامپوننت دربرگیرنده، ارسال می‌کند. نحوه‌ی تعریف این رشته هم توسط back tick مربوط به ES 6 صورت گرفته‌است.

              تا اینجا مرحله‌ی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کننده‌ی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.

              اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
              الف) ابتدا نام خاصیت مزین شده‌ی با Output، به عنوان مقصد event binding مشخص می‌شود و سپس متدی در کلاس کامپوننت والد، به آن متصل می‌گردد:
               (ratingClicked)='onRatingClicked($event)'
              event$ مقدار ارسالی از کامپوننت فرزند را به کامپوننت والد منتقل می‌کند.
              ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه می‌کنیم:
              onRatingClicked(message: string): void {
                  this.pageTitle = 'Product List: ' + message;
              }
              نوع پارامتر ورودی این متد را با توجه به نوع رشته‌ای <EventEmitter<string در کلاس StarComponent  تعریف کردیم.
              به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستاره‌ای امتیازها، خاصیت pageTitle درج شده‌ی در صفحه تغییر می‌کند.


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

              نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شده‌ی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشه‌ی سایت ندارد و یکی هستند.
              برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر می‌دهیم:
              <td>
                  <ai-star [rating]='product.starRating'
                           (ratingClicked)='onRatingClicked($event)'>
                  </ai-star>
              </td>
              همانطور که ملاحظه می‌کنید، ابتدا selector این کامپوننت جدید، به صورت یک المان جدید و سفارشی HTML، در قالب کامپوننت لیست محصولات درج می‌شود. همچنین خاصیت rating ورودی آن به عدد امتیاز محصول جاری در حال رندر، متصل شده‌است. به علاوه توسط event binding به خاصیت ratingClicked که از نوع ()Output@ تعریف شده‌است، متد onRatingClicked متصل گردیده‌است.

              سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی می‌کنیم:
              import { Component, OnInit } from 'angular2/core';
              import { IProduct } from './product';
              import { ProductFilterPipe } from './product-filter.pipe';
              import { StarComponent } from '../shared/star.component';
               
              @Component({
                  selector: 'pm-products',
                  templateUrl: 'app/products/product-list.component.html',
                  styleUrls: ['app/products/product-list.component.css'],
                  pipes: [ProductFilterPipe],
                  directives: [StarComponent]
              })
              در اینجا دو کار جدید انجام شده‌است. ابتدا خاصیت directives، به نام کلاس این کامپوننت جدید (StarComponent)، تنظیم شده‌است. سپس تعریف import ماژول این کامپوننت، به ابتدای فایل جاری اضافه شده‌است.

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

              در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستاره‌ای این امتیازها جایگزین شده‌است و همچنین با کلیک بر روی یکی از آن‌ها، عنوان panel جاری تغییر کرده‌است.


              کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part6.zip



              خلاصه‌ی بحث

              در اینجا نحوه‌ی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف می‌شوند، میدان دید آن‌ها محدود است به قالب تعریف شده‌ی متناظر با آن‌ها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرنده‌ی دیگری در دسترس قرار گیرد، آن‌را با مزین کننده‌ی ()Input@ مشخص می‌کنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، این‌کار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام می‌دهد. نوع آرگومان جنریک EventEmitter، تعیین کننده‌ی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
              پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده می‌شود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال می‌گردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده می‌شود. در اینجا هر رخدادی که توسط مزین کننده‌ی ()Output@ تعریف شده باشد، می‌تواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، می‌توان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
              مطالب
              مبانی TypeScript؛ Decorators
              Decorators  یا تزئین کننده‌ها و ReflectDecorators، یکی از پیشنهادهای نگارش بعدی جاوا اسکریپت هستند (ECMAScript 2016) که هم اکنون قابلیت استفاده‌ی از آن‌ها در TypeScript وجود دارد.
              جهت افزودن قابلیت‌های meta-programming به زبان‌های جاوا اسکریپت و TypeScript و همچنین تعریف annotations بر روی کلاس‌ها و اعضای کلاس، می‌توان از Decorators استفاده کرد. یک decorator، تعریف ویژه‌ای است که می‌تواند به تعریف یک کلاس، متد، خاصیت و یا پارامتر «متصل» شود و به صورت expression@ تعریف می‌گردد. این expression باید قابلیت فراخوانی به صورت یک متد را داشته باشد که در زمان اجرا فراخوانی خواهد شد. از Decorators در طراحی AngularJS 2 زیاد استفاده شده‌است.


              نحوه‌ی فعال سازی Decorators با ES 5

              این قابلیت فعلا در مرحله‌ی آزمایش به سر می‌برد؛ بنابراین برای فعال سازی آن نیاز است پارامترهای experimentalDecorators و emitDecoratorMetadata را به کامپایلر خط فرمان tsc و یا به خواص کامپایلر در فایل tsconfig.json اضافه کنید:
              Command Line:
              tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
              
              tsconfig.json:
              {
                  "compilerOptions": {
                      "target": "ES5",
                      "experimentalDecorators": true,
                      "emitDecoratorMetadata": true
                  }
              }


              بررسی انواع Decorators

              در اینجا یک مثال ساده از decoratorها را مشاهده می‌کنید:
               // A simple decorator
              @decoratorExpression
              class MyClass { }
              و ساده‌ترین فرم پیاده سازی این decoratorExpression به صورت زیر است:
              function decoratorExpression(target) {
                 // Add a property on target
                 target.annotated = true;
              }
              همانطور که ملاحظه می‌کنید، یک decorator در اصل یک function است که دسترسی به target ایی را که قرار است تزئین شود، میسر می‌کند. این قابلیت بسیار شبیه است به مفهومی به نام attributes و annotations در زبان #C.
              در ادامه انواع و اقسام decoratorهای ممکن را با مثال‌هایی بررسی خواهیم کرد.


              Class Decorator

              یک class Decorator، پیش از تعریف یک کلاس اضافه می‌شود و هدف آن، اعمال تعاریفی به سازنده‌ی کلاس است و از آن می‌توان جهت تحت نظر قرار دادن تعاریف کلاس و یا تغییر یا حتی تعویض کلی تعاریف آن استفاده کرد. یک class Decorator را نمی‌توان در سایر فایل‌های تعاریف، مانند d.ts. قرار داد.
              تنها آرگومانی که به یک class Decorator ارسال می‌شود، سازنده‌ی کلاسی است که به آن اعمال شده‌است. اگر متد class Decorator مقداری را برگرداند، سبب تعویض و جایگزینی تعاریف کلاس با مقدار باگشت داده شده، می‌شود.
              در ادامه دو مثال را از class Decoratorها مشاهده می‌کنید:

              مثال بدون پارامتر
               function ClassDecorator(
              target: Function // The class the decorator is declared on
              ) {
                  console.log("ClassDecorator called on: ", target);
              }
              
              @ClassDecorator
              class ClassDecoratorExample {
              }


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

              مثال با پارامتر
               function ClassDecoratorParams(param1: number, param2: string) {
              return function(
                target: Function // The class the decorator is declared on
                ) {
                 console.log("ClassDecoratorParams(" + param1 + ", '" + param2 + "') called on: ", target);
              }
              }
              
              @ClassDecoratorParams(1, "a")
              @ClassDecoratorParams(2, "b")
              class ClassDecoratorParamsExample {
              }
              با خروجی
               ClassDecoratorParams(2, 'b') called on: function ClassDecoratorParamsExample() {
              }
              ClassDecoratorParams(1, 'a') called on: function ClassDecoratorParamsExample() {
              }
              همانطور که در این مثال مشاهده می‌کنید، چندین decorator را نیز می‌توان به تعاریف یک کلاس اعمال کرد که به آن Decorator Composition نیز می‌گویند.


              Property Decorator

              یک Property Decorator دقیقا پیش از تعریف یک خاصیت اضافه می‌شود و نباید در سایر فایل‌های تعاریف جانبی قرار گیرد. زمانیکه متد expression آن در runtime فراخوانی می‌شود، دو پارامتر را دریافت خواهد کرد:
              الف) برای static member ها، متد سازنده‌ی کلاس و برای  instance memberها، prototype کلاس را دریافت می‌کند.
              ب) نام عضو.

              اگر متد expression تعریف شده، مقداری را برگرداند، به عنوان Property Descriptor آن عضو بکارگرفته می‌شود.

              در مثال ذیل که متد PropertyDecorator به خاصیت name اعمال شده‌است، دو پارامتر ارسالی به آن‌را بهتر می‌توان مشاهده کرد:
               function PropertyDecorator(
              target: Object, // The prototype of the class
              propertyKey: string | symbol // The name of the property
              ) {
                 console.log("PropertyDecorator called on: ", target, propertyKey);
              }
              
              class PropertyDecoratorExample {
              @PropertyDecorator
              name: string;
              }
              با خروجی
               PropertyDecorator called on: PropertyDecoratorExample {} name


              Method Decorator

              یک Method Decorator باید درست پیش از تعریف یک متد، قرارگیرد و نباید در سایر کلاس‌ها تعاریف نوع‌های جانبی افزوده شود. هدف از آن می‌تواند بررسی، مشاهده و تغییر رفتار یک متد باشد. به متد expression آن، سه پارامتر ارسال می‌شوند:
              الف) برای static member ها، متد سازنده‌ی کلاس و برای  instance memberها، prototype کلاس را دریافت می‌کند.
              ب) نام عضو
              ج) Property Descriptor عضو

              اگر متد تعریف شده، خروجی را برگرداند به عنوان Property Descriptor آن متد استفاده خواهد شد.

              در مثال ذیل، متد تزئین کننده‌ای به نام MethodDecorator تعریف شده‌است که سه پارامتر یاد شده را در زمان اجرا دریافت می‌کند:
               function MethodDecorator(
              target: Object, // The prototype of the class
              propertyKey: string, // The name of the method
              descriptor: TypedPropertyDescriptor<any>
              ) {
              console.log("MethodDecorator called on: ", target, propertyKey, descriptor);
              }
              
              class MethodDecoratorExample {
                 @MethodDecorator
                 method() {
                 }
              }
              با خروجی
               MethodDecorator called on: MethodDecoratorExample { method: [Function] } method { value: [Function],
              writable: true,
              enumerable: true,
              configurable: true }

              و مثالی جهت محدود ساختن آن به یک سری متد خاص که یک پارامتر عددی را دریافت می‌کنند و یک خروجی عددی نیز دارند:
               function TypeRestrictedMethodDecorator(
              target: Object, // The prototype of the class
              propertyKey: string, // The name of the method
              descriptor: TypedPropertyDescriptor<(num: number) => number>
              ) {
                 console.log("TypeRestrictedMethodDecorator called on: ", target, propertyKey, descriptor);
              }
              
              class TypeRestrictedMethodDecoratorExample {
                 @TypeRestrictedMethodDecorator
                 method(num: number): number {
                    return 0;
                 }
              }
              با خروجی
               TypeRestrictedMethodDecorator called on: TypeRestrictedMethodDecoratorExample { method: [Function] } method { value: [Function],
              writable: true,
              enumerable: true,
              configurable: true }

              و مثالی از تزئین کننده‌های متدهای استاتیک یک کلاس که در این حالت، پارامتر target از نوع متد سازنده‌ی کلاس خواهد بود و نه prototype آن:
               function StaticMethodDecorator(
              target: Function, // the function itself and not the prototype
              propertyKey: string | symbol, // The name of the static method
              descriptor: TypedPropertyDescriptor<any>
              ) {
                 console.log("StaticMethodDecorator called on: ", target, propertyKey, descriptor);
              }
              
              class StaticMethodDecoratorExample {
                 @StaticMethodDecorator
                 static staticMethod() {
                 }
              }
              با خروجی
               StaticMethodDecorator called on: function StaticMethodDecoratorExample() {
              } staticMethod { value: [Function],
              writable: true,
              enumerable: true,
              configurable: true }


              Parameter Decorator

              یک Parameter Decorator باید درست پیش از تعریف یک آرگومان متد، قرارگیرد و نباید در سایر کلاس‌ها تعاریف نوع‌های جانبی افزوده شود. به متد expression آن سه پارامتر ارسال می‌شوند:
              الف) برای static member ها، متد سازنده‌ی کلاس و برای  instance memberها، prototype کلاس را دریافت می‌کند.
              ب) نام عضو
              ج) شماره ایندکس پارامتر مدنظر در متد

              از خروجی متد تزئین کننده در اینجا صرفنظر می‌شود.

              در ادامه مثالی را از نحوه‌ی تعریف یک تزئین کننده‌ی پارامترها را با سه آرگومان ویژه‌ی آن، مشاهده می‌کنید:
               function ParameterDecorator(
              target: Function, // The prototype of the class
              propertyKey: string | symbol, // The name of the method
              parameterIndex: number // The index of parameter in the list of the function's parameters
              ) {
                 console.log("ParameterDecorator called on: ", target, propertyKey, parameterIndex);
              }
              
              class ParameterDecoratorExample {
                 method(@ParameterDecorator param1: string, @ParameterDecorator param2: number) {
                 }
              }
              با خروجی
               ParameterDecorator called on: ParameterDecoratorExample { method: [Function] } method 1
              ParameterDecorator called on: ParameterDecoratorExample { method: [Function] } method 0

              یک سری مثال تکمیلی
              مطالب
              مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت اول - Redux چیست؟
              Redux و Mobx، کتابخانه‌های کمکی هستند برای مدیریت حالت برنامه‌های پیچیده‌ی React. هرچند React به صورت توکار به همراه امکانات مدیریت حالت است، اما این کتابخانه‌ها مزایای ویژه‌ای را به آن اضافه می‌کنند. در این سری ابتدا کتابخانه‌ی Redux را به صورت خالص و مجزای از React بررسی می‌کنیم. از این کتابخانه در برنامه‌های Angular و Ember هم می‌توان استفاده کرد و به صورت اختصاصی برای React طراحی نشده‌است. سپس آن‌را به برنامه‌های React متصل می‌کنیم. در آخر کتابخانه‌ی محبوب دیگری را به نام Mobx بررسی می‌کنیم که برای مدیریت حالت، اصول برنامه نویسی شیءگرا و همچنین Reactive را با هم ترکیب می‌کند و این روزها در برنامه‌های React، بیشتر از Redux مورد استفاده قرار می‌گیرد.


              چرا به ابزارهای مدیریت حالت نیاز داریم؟

              به محض رد شدن از مرز پیاده سازی امکانات اولیه‌ی یک برنامه، نیاز به ابزارهای مدیریت حالت نمایان می‌شوند؛ خصوصا زمانیکه نیاز است با اطلاعات قابل توجهی سر و کار داشت. مهم‌ترین دلیل استفاده‌ی از یک ابزار مدیریت حالت، مدیریت منطق تجاری برنامه است. منطق نمایشی برنامه مرتبط است به نحوه‌ی نمایش اجزای آن در صفحه؛ مانند نمایش یک صفحه‌ی مودال، تغییر رنگ عناصر با عبور کرسر ماوس از روی آن‌ها و در کل منطقی که مرتبط و یا وابسته‌ی به هدف اصلی برنامه نیست. از سوی دیگر منطق تجاری برنامه مرتبط است با مدیریت، تغییر و ذخیره سازی اشیاء تجاری مورد نیاز آن؛ مانند اطلاعات حساب کاربری شخص و دریافت اطلاعات برنامه از یک API که مختص به برنامه‌ی خاص ما است و به همین دلیل نیاز به ابزاری برای مدیریت بهینه‌ی آن وجود دارد. برای مثال اینکه در کجا باید منطق تجاری و نمایشی را به هم متصل کرد، می‌تواند چالش بر انگیر باشد. چگونه باید اطلاعات کاربر را ذخیره کرد؟ چگونه React باید متوجه شود که اطلاعات ما تغییر کرده‌است و در نتیجه‌ی آن کامپوننتی را مجددا رندر کند؟ یک ابزار مدیریت حالت، تمام این مسایل را به نحو یک‌دستی در سراسر برنامه، مدیریت می‌کند.
              اگر از یک ابزار مدیریت حالت استفاده نکنیم، مجبور خواهیم شد تمام اطلاعات منطق تجاری را در داخل state کامپوننت‌ها ذخیره کنیم که توصیه نمی‌شود؛ چون مقیاس پذیر نیست. برای مثال فرض کنید قرار است تمام اطلاعات state را داخل یک کامپوننت ذخیره کنیم. هر زمانیکه بخواهیم این state را از طریق یک کامپوننت فرزند تغییر دهیم، نیاز خواهد بود این اطلاعات را به والد آن کامپوننت ارسال کنیم که اگر از تعداد زیادی کامپوننت تو در تو تشکیل شده باشد، زمانبر و به همراه کدهای تکراری زیادی خواهد بود. همچنین اینکار سبب رندر مجدد کل برنامه با هر تغییری در state آن می‌شود که غیرضروری بوده و کارآیی برنامه را کاهش می‌دهد. به علاوه در این بین مشخص نیست هر قسمت از state، از کدام کامپوننت تامین شده‌است. به همین جهت نیاز به روشی برای مدیریت حالت در بین کامپوننت‌های برنامه وجود دارد.


              داشتن تنها یک محل برای ذخیره سازی state در برنامه

              همانطور که در قسمت 8 ترکیب کامپوننت‌ها در سری React 16x بررسی کردیم، هر کامپوننت در React، دارای state خاص خودش است و این state از سایر کامپوننت‌ها کاملا مستقل و ایزوله‌است. این مورد با بزرگ‌تر شدن برنامه و برقراری ارتباط بین کامپوننت‌ها، مشکل ایجاد می‌کند. برای مثال اگر بخواهیم دکمه‌ای را در صفحه قرار داده و توسط این دکمه درخواست صفر شدن مقدار هر کدام از شمارشگرها را صادر کنیم، با صفر کردن value هر کدام از این کامپوننت‌ها، اتفاقی رخ نمی‌دهد. چون state محلی این کامپوننت‌ها، با سایر اجزای صفحه به اشتراک گذاشته نمی‌شود و باید آن‌را تبدیل به یک controlled component کرد، بطوریکه دارای local state خاص خودش نیست و تمام داده‌های دریافتی را از طریق this.props دریافت می‌کند و هر زمانیکه قرار است داده‌ای تغییر کند، رخ‌دادی را به والد خود صادر می‌کند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل می‌شود. تازه این روش در مورد کامپوننت‌هایی صدق می‌کند که رابطه‌ی والد و فرزندی بین آن‌ها وجود دارد. اگر چنین رابطه‌ای وجود نداشت، باید state را به یک سطح بالاتر انتقال داد. برای مثال باید state کامپوننت Counters را به والد آن که کامپوننت App است، منتقل کرد. پس از آن چون کامپوننت‌های ما، از کامپوننت App مشتق می‌شوند، اکنون می‌توان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت. این مورد هم مانند مثال انتقال اطلاعات کاربر لاگین شده‌ی به سیستم، به تمام زیر قسمت‌های برنامه، نیاز به ارسال اطلاعات از طریق props یک کامپوننت، به کامپوننت بعدی را دارد و به همین ترتیب برای مابقی که به props drilling مشهور است و روش پسندیده‌ای نیست.


              Redux چیست؟ ذخیره سازی کل درخت state یک برنامه، در یک محل. به این ترتیب به یک شیء جاوا اسکریپتی بزرگ خواهیم رسید که در برگیرنده‌ی تمام state برنامه‌است. یکی از مزایای آن امکان serialize و deserialize کل این شیء، به سادگی است. برای مثال توسط متد JSON.stringify می‌توان آن‌را در جائی ذخیره کرد و سپس آن‌را به صورت یک شیء جاو اسکریپتی در زمانی دیگر بازیابی کرد. یکی از مزایای آن، امکان بازیابی دقیق شرایط کاربری است که دچار مشکل شده‌است و سپس دیباگ و رفع مشکل او، در زمانی دیگر.


              تاریخچه‌ای از سیستم‌های مدیریت حالت

              همه چیز با AngularJS 1x شروع شد که از data binding دو طرفه پشتیبانی می‌کرد. هرچند این روش برای همگام نگه داشتن View و مدل برنامه، مفید است، اما در Viewهای پیچیده، برنامه را کند می‌کند. در همین زمان فیس‌بوک، روش مدیریت حالتی را به نام Flux ارائه داد که از data binding یک طرفه پشتیبانی می‌کرد. به این معنا که در این روش، همواره اطلاعات از View به مدل، جریان پیدا می‌کند. کار کردن با آن ساده‌است؛ چون نیازی نیست حدس زده شود که اکنون جریان اطلاعات از کدام سمت است. اما مشکل آن عدم هماهنگی model و view، در بعضی از حالات است. Flux از این جهت به وجود آمد که مدیریت حالت در برنامه‌های React آن زمان، پیچیده بود و مقیاس پذیری کمی داشت (پیش از ارائه‌ی Context و Hooks). در کل Flux صرفا یکسری الگوی مدیریت حالت را بیان می‌کند و یک کتابخانه‌ی مجزا نیست. بر مبنای این الگوها و قراردادها، می‌توان کتابخانه‌های مختلفی را ایجاد کرد. از این رو در سال 2015، کتابخانه‌های زیادی مانند Reflux, Flummox, MartyJS, Alt, Redux و غیره برای پیاده سازی آن پدید آمدند. در این بین، کتابخانه‌ی Redux ماندگار شد و پیروز این نبرد بود!


              توابع خالص و ناخالص (Pure & Impure Functions)

              پیش از شروع بحث، نیاز است با یک‌سری از واژه‌ها مانند توابع خالص و ناخالص آشنا شد. این نکات از این جهت مهم هستند که Redux فقط با توابع خالص کار می‌کند.
              توابع خالص: تعدادی آرگومان را دریافت کرده و بر اساس آن‌ها، مقداری را باز می‌گردانند.
              // Pure
              const add = (a, b) => {
                return a + b;
              }
              در اینجا یک تابع خالص را مشاهده می‌کنید که a و b را دریافت کرده و بر این اساس، یک خروجی کاملا مشخص را بازگشت می‌دهد.

              توابع ناخالص: این نوع توابع سبب تغییراتی در متغیرهایی خارج از میدان دید خود می‌شوند و یا به همراه یک سری اثرات جانبی (side effects) مانند تعامل با دنیای خارج (وجود یک console.log در آن تابع و یا دریافت اطلاعاتی از یک API خارجی) هستند.
              // Impure
              const b;
              
              const add = (a) => {
                return a + b;
              }
              تابع تعریف شده‌ی در اینجا ناخالص است؛ چون با اطلاعاتی خارج از میدان دید خود مانند متغیر b، تعامل دارد. این تعامل با دنیای خارج، حتی در حد نوشتن یک console.log:
              // Impure
              const add = (a, b) => {
                console.log('lolololol');
                return a + b;
              }
              یک تابع خالص را تبدیل به یک تابع ناخالص می‌کند و یا نمونه‌ی دیگر این تعاملات، فراخوانی سرویس‌های backend در برنامه هستند که یک تابع را ناخالص می‌کنند:
              // Impure
              const add = (a, b) => {
                 Api.post('/add', { a, b }, (response) => {
                  // Do something.
                 });
              };


              روش‌هایی برای جلوگیری از تغییرات در اشیاء در جاوا اسکریپت

              ایجاد تغییرات در آرایه‌ها و اشیاء (Mutating arrays and objects) نیز ناخالصی ایجاد می‌کند؛ از این جهت که سبب تغییراتی در دنیای خارج (خارج از میدان دید تابع) می‌شویم. به همین جهت نیاز به روش‌هایی وجود دارد که از این نوع تغییرات جلوگیری کرد:
              // Copy object
              const original = { a: 1, b: 2 };
              const copy = Object.assign({}, original);
              برای تغییری در یک شیء، تنها کافی است خاصیتی را به آن اضافه کنیم و یا با استفاده از واژه‌ی کلیدی delete، خاصیتی را از آن حذف کنیم. به همین جهت برای اینکه تغییرات ما بر روی شیء اصلی اثری را باقی نگذارند، یکی از روش‌ها، استفاده از متد Object.assign است. کار آن، یکی کردن اشیایی است که به آن ارسال می‌شوند. به همین جهت در اینجا با یک شیء خالی، از صفر شروع می‌کنیم. سپس دومین آرگومان آن را به همان شیء مدنظر، تنظیم می‌کنیم. به این ترتیب به یک کپی از شیء اصلی می‌رسیم که دیگر به آن، اتصالی را ندارد. به همین جهت اگر بر روی این شیء کپی تغییراتی را ایجاد کنیم، به شیء اصلی کپی نمی‌شود و سبب تغییرات در آن (mutation) نخواهد شد.
              برای مثال در React، برای انجام رندر نهایی، در پشت صحنه کار مقایسه‌ی اشیاء صورت می‌گیرد. به همین جهت اگر همان شیءای را که ردیابی می‌کند تغییر دهیم، دیگر نمی‌تواند به صورت مؤثری فقط قسمت‌های تغییر کرده‌ی آن‌را تشخیص داده و کار رندر را فقط بر اساس آن‌ها انجام دهد و مجبور خواهد شد کل یک شیء را بارها و بارها رندر کند که اصلا بهینه نیست. به همین جهت، ایجاد تغییرات مستقیم در شیءای که به state آن انتساب داده می‌شود، مجاز نیست.

              متد Object.assign، چندین شیء را نیز می‌تواند با هم یکی کند و شیء جدیدی را تشکیل دهد:
              // Extend object
              const original = { a: 1, b: 2 };
              const extension = { c: 3 };
              const extended = Object.assign({}, original, extension);
              روش دیگر ایجاد یک کپی و یا clone از یک شیء را که پیشتر در سری «React 16x» بررسی کردیم، به کمک امکانات ES-6، به صورت زیر است:
              // Copy object
              const original = { a: 1, b: 2 };
              const copy = { ...original };
              در اینجا نیز ابتدا یک شیء خالی را ایجاد می‌کنیم و سپس توسط spread operator، خواص شیء قبلی را درون آن باز کرده و قرار می‌دهیم. به این ترتیب به یک clone از شیء اصلی می‌رسیم. این حالت نیز از ترکیب چندین شیء با هم، پشتیبانی می‌کند:
              // Extend object
              const original = { a: 1, b: 2 };
              const extension = { c: 3 };
              const extended = { ...original, ...extension };


              روش‌هایی برای جلوگیری از تغییرات در آرایه‌ها در جاوا اسکریپت

              متد slice آرایه‌ها نیز بدون ذکر آرگومانی، یک کپی از آرایه‌ی اصلی را ایجاد می‌کند:
              // Copy array
              const original = [1, 2, 3];
              const copy = [1, 2, 3].slice();
              همچنین معادل همین قطعه کد در ES-6 به همراه spread operator به صورت زیر است:
              // Copy array
              const original = [1, 2, 3];
              const copy = [ ...original ];
              و یا اگر بخواهیم یک کپی از چندین آرایه را ایجاد کنیم می‌توان از متد concat استفاده کرد:
              // Extend array
              const original = [1, 2, 3];
              const extended = original.concat(4);
              const moreExtended = original.concat([4, 5]);
              متد Array.push، هرچند سبب افزوده شدن عنصری به یک آرایه می‌شود، اما یک mutation را نیز ایجاد می‌کند؛ یعنی تغییرات آن به دنیای خارج اعمال می‌گردد. اما Array.concat یک آرایه‌ی کاملا جدید را ایجاد می‌کند و همچنین امکان ترکیب آرایه‌ها را نیز به همراه دارد.
              معادل قطعه کد فوق در ES-6 و به همراه spread operator آن به صورت زیر است:
              // Extend array
              const original = [1, 2, 3];
              const extended = [ ...original, 4 ];
              const moreExtended = [ ...original, ...extended, 5 ];


              مفاهیم ابتدایی Redux


              در Redux برای ایجاد تغییرات در شیء کلی state، از مفهومی به نام dispatch actions استفاده می‌شود. action در اینجا به معنای رخ‌دادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسه‌ای بین وضعیت قبلی state و وضعیت فعلی آن صورت می‌گیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد.
              اصلی‌ترین جزء Redux، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت می‌کند:


              تابع Reducer، بر اساس action و یا رخ‌دادی، ابتدا کل state برنامه را دریافت می‌کند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامه‌ی React ما می‌تواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند. به این ترتیب کار اصلی مدیریت state، به خارج از برنامه‌ی React منتقل می‌شود.

              در این تصویر، تابع action creator را هم ملاحظه می‌کند که کاملا اختیاری است. یک action می‌تواند یک رشته و یا یک عدد باشد. با پیچیده شدن برنامه، نیاز به ارسال یک‌سری متادیتا و یا اطلاعات بیشتری از اکشن رسیده‌است. کار action creator، ایجاد شیء action، به صورت یک دست و یکنواخت است تا دیگر نیازی به ایجاد دستی آن نباشد.


              مزایای کار با Redux

              - داشتن یک مکان مرکزی برای ذخیره سازی کلی حالت برنامه (به آن «source of truth» و یا store هم گفته می‌شود): به این ترتیب مشکل ارسال خواص در بین کامپوننت‌های عمیق و چند سطحی، برطرف شده و هر زمانیکه نیاز بود، از آن اطلاعاتی را دریافت و یا با قالب خاصی، آن‌را به روز رسانی می‌کنند.
              - رسیدن به به‌روز رسانی‌های قابل پیش بینی state: هرچند در حالت کار با Redux، یک شیء بزرگ جاوا اسکریپتی، کل state برنامه را تشکیل می‌دهد، اما امکان کار مستقیم با آن و تغییرش وجود ندارد. به همین جهت است که برای کار با آن، باید رویدادی را از طریق actionها به تابع Reducer آن تحویل داد. چون Reducer یک تابع خالص است، با دریافت یک سری ورودی مشخص، همواره یک خروجی مشخص را نیز تولید می‌کند. به همین جهت قابلیت ضبط و تکرار را پیدا می‌کند؛ همان بحث serialize و deseriliaze، توسط ابزاری مانند: logrocket. به علاوه قابلیت undo و redo را نیز می‌توان به این ترتیب پیاده سازی کرد (state جدید محاسبه شده، مشخص است، کل state قبلی را نیز داریم یا می‌توان ذخیره کرد و سپس برای undo، آن‌را جایگزین state جدید نمود). افزونه‌ی redux dev tools نیز قابلیت import و export کل state را به همراه دارد.
              - چون تابع Reducer، یک تابع خالص است و همواره خروجی‌های مشخصی را به ازای ورودی‌های مشخصی، تولید می‌کند، آزمایش کردن، پیاده سازی و حتی logging آن نیز ساده‌تر است. در این بین حتی یک افزونه‌ی مخصوص نیز برای دیباگ آن تهیه شده‌است: redux-devtools-extension. تابع خالص، تابعی است که به همراه اثرات جانبی نیست (side effects)؛ به همین جهت عملکرد آن کاملا قابل پیش بینی بوده و آزمون پذیری آن به دلیل نداشتن وابستگی‌های خارجی، بسیار بالا است.


              Context API خود React چطور؟

              در قسمت 33 سری React 16x، مفهوم React Context را بررسی کردیم. پس از معرفی آن با React 16.3، مقالات زیادی منتشر شدند که ... Redux مرده‌است (!) و یا بجای Redux از React context استفاده کنید. اما واقعیت این است که React Redux در پشت صحنه از React context استفاده می‌کند و تابع connect آن دقیقا به همین زیر ساخت متصل می‌شود.
              کار با Redux مزایایی مانند کارآیی بالاتر، با کاهش رندر‌های مجدد کامپوننت‌ها، دیباگ ساده‌تر با افزونه‌های اختصاصی و همچنین سفارشی سازی، مانند نوشتن میان‌افزارها را به همراه دارد. اما شاید واقعا نیازی به تمام این امکانات را هم نداشته باشید؛ اگر هدف، صرفا انتقال ساده‌تر اطلاعات بوده و برنامه‌ی مدنظر نیز کوچک است. React Context برخلاف Redux، نگهدارنده‌ی state نیست و بیشتر هدفش محلی برای ذخیره سازی اطلاعات مورد استفاده‌ی در چندین و چند کامپوننت تو در تو است. هرچند شبیه به Redux می‌توان اشاره‌گرهایی از متدها را به استفاده کنندگان از آن ارسال کرد تا سبب بروز رویدادها و اکشن‌هایی در کامپوننت تامین کننده‌ی Contrext شوند (یا یک کتابخانه‌ی ابتدایی شبیه به Redux را توسط آن تهیه کرد). بنابراین برای انتخاب بین React Context و Redux باید به اندازه‌ی برنامه، تعداد نفرات تیم، آشنایی آن‌ها با مفاهیم Redux دقت داشت.
              مطالب
              زیرنویس فارسی ویدئوهای مقدمات AngularJS - قسمت سوم
              زیرنویس‌های فارسی قسمت سوم را اینجا می‌توانید دریافت کنید.
              لیست سرفصل‌های قسمت سوم به شرح زیر است :
              01. Introduction to Services
              02. Demo - Creating Your First Custom Service
              03. Demo - Another Custom Service Example
              04. Introduction to Built-In AngularJS Services
              05. Demo - Using the $http and $q Services Together
              06. Demo - Using the $rsource and $q Services
              07. Demo - Using the $anchorScroll Service
              08. Demo - Using the $cacheFactory Service
              09. Demo - Using the $compile Service
              10. Demo - Using the $parse Service
              11. Demo - Using the $locale Service
              12. Demo - Using the $timeout Service
              13. Demo - Using the $exceptionHandler Service
              14. Demo - Using the $filter Service
              15. Demo - Using the $cookieStore Service
              16. Overview of Less Common Services
              17. Suggested Exercises
              این قسمت به ساخت سرویس‌های سفارشی و همچنین چگونگی استفاده از سرویس‌های توکار انگولار می‌پردازد؛ در این قسمت تقریباً تمامی سرویس‌های موردنیاز جهت توسعه یک برنامه مبتنی بر انگولار شرح داده می‌شود. همچنین در آن به صورت عملی با سرویس‌ها آشنا می‌شوید و هر سرویسی که معرفی میشود مثال مربوط به آن ارائه شده است. به طور مثال در قسمت پیاده سازی سرویس‌های سفارشی مثال نمایش Gravatar با وارد کردن آدرس ایمیل کاربر مطرح می‌شود که در ادامه کد آن را مشاهده میکنید :
              'use strict';
              
              eventsApp.factory('gravatarUrlBuilder', function() {
              return {
              buildGravatarUrl: function(email) {
                      // try angularjsdemo@gmail.com as an email
                          var defaultGravatarUrl = "http://www.gravatar.com/avatar/000?s=200";
              
                          var regex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
                          if (!regex.test(email))
                              return defaultGravatarUrl;
              
                          var MD5=function(s){function L(k,d){return(k<<d)|(k>>>(32-d))}function K(G,k){var I,d,F,H,x;F=(G&2147483648);H=(k&2147483648);I=(G&1073741824);d=(k&1073741824);x=(G&1073741823)+(k&1073741823);if(I&d){return(x^2147483648^F^H)}if(I|d){if(x&1073741824){return(x^3221225472^F^H)}else{return(x^1073741824^F^H)}}else{return(x^F^H)}}function r(d,F,k){return(d&F)|((~d)&k)}function q(d,F,k){return(d&k)|(F&(~k))}function p(d,F,k){return(d^F^k)}function n(d,F,k){return(F^(d|(~k)))}function u(G,F,aa,Z,k,H,I){G=K(G,K(K(r(F,aa,Z),k),I));return K(L(G,H),F)}function f(G,F,aa,Z,k,H,I){G=K(G,K(K(q(F,aa,Z),k),I));return K(L(G,H),F)}function D(G,F,aa,Z,k,H,I){G=K(G,K(K(p(F,aa,Z),k),I));return K(L(G,H),F)}function t(G,F,aa,Z,k,H,I){G=K(G,K(K(n(F,aa,Z),k),I));return K(L(G,H),F)}function e(G){var Z;var F=G.length;var x=F+8;var k=(x-(x%64))/64;var I=(k+1)*16;var aa=Array(I-1);var d=0;var H=0;while(H<F){Z=(H-(H%4))/4;d=(H%4)*8;aa[Z]=(aa[Z]|(G.charCodeAt(H)<<d));H++}Z=(H-(H%4))/4;d=(H%4)*8;aa[Z]=aa[Z]|(128<<d);aa[I-2]=F<<3;aa[I-1]=F>>>29;return aa}function B(x){var k="",F="",G,d;for(d=0;d<=3;d++){G=(x>>>(d*8))&255;F="0"+G.toString(16);k=k+F.substr(F.length-2,2)}return k}function J(k){k=k.replace(/rn/g,"n");var d="";for(var F=0;F<k.length;F++){var x=k.charCodeAt(F);if(x<128){d+=String.fromCharCode(x)}else{if((x>127)&&(x<2048)){d+=String.fromCharCode((x>>6)|192);d+=String.fromCharCode((x&63)|128)}else{d+=String.fromCharCode((x>>12)|224);d+=String.fromCharCode(((x>>6)&63)|128);d+=String.fromCharCode((x&63)|128)}}}return d}var C=Array();var P,h,E,v,g,Y,X,W,V;var S=7,Q=12,N=17,M=22;var A=5,z=9,y=14,w=20;var o=4,m=11,l=16,j=23;var U=6,T=10,R=15,O=21;s=J(s);C=e(s);Y=1732584193;X=4023233417;W=2562383102;V=271733878;for(P=0;P<C.length;P+=16){h=Y;E=X;v=W;g=V;Y=u(Y,X,W,V,C[P+0],S,3614090360);V=u(V,Y,X,W,C[P+1],Q,3905402710);W=u(W,V,Y,X,C[P+2],N,606105819);X=u(X,W,V,Y,C[P+3],M,3250441966);Y=u(Y,X,W,V,C[P+4],S,4118548399);V=u(V,Y,X,W,C[P+5],Q,1200080426);W=u(W,V,Y,X,C[P+6],N,2821735955);X=u(X,W,V,Y,C[P+7],M,4249261313);Y=u(Y,X,W,V,C[P+8],S,1770035416);V=u(V,Y,X,W,C[P+9],Q,2336552879);W=u(W,V,Y,X,C[P+10],N,4294925233);X=u(X,W,V,Y,C[P+11],M,2304563134);Y=u(Y,X,W,V,C[P+12],S,1804603682);V=u(V,Y,X,W,C[P+13],Q,4254626195);W=u(W,V,Y,X,C[P+14],N,2792965006);X=u(X,W,V,Y,C[P+15],M,1236535329);Y=f(Y,X,W,V,C[P+1],A,4129170786);V=f(V,Y,X,W,C[P+6],z,3225465664);W=f(W,V,Y,X,C[P+11],y,643717713);X=f(X,W,V,Y,C[P+0],w,3921069994);Y=f(Y,X,W,V,C[P+5],A,3593408605);V=f(V,Y,X,W,C[P+10],z,38016083);W=f(W,V,Y,X,C[P+15],y,3634488961);X=f(X,W,V,Y,C[P+4],w,3889429448);Y=f(Y,X,W,V,C[P+9],A,568446438);V=f(V,Y,X,W,C[P+14],z,3275163606);W=f(W,V,Y,X,C[P+3],y,4107603335);X=f(X,W,V,Y,C[P+8],w,1163531501);Y=f(Y,X,W,V,C[P+13],A,2850285829);V=f(V,Y,X,W,C[P+2],z,4243563512);W=f(W,V,Y,X,C[P+7],y,1735328473);X=f(X,W,V,Y,C[P+12],w,2368359562);Y=D(Y,X,W,V,C[P+5],o,4294588738);V=D(V,Y,X,W,C[P+8],m,2272392833);W=D(W,V,Y,X,C[P+11],l,1839030562);X=D(X,W,V,Y,C[P+14],j,4259657740);Y=D(Y,X,W,V,C[P+1],o,2763975236);V=D(V,Y,X,W,C[P+4],m,1272893353);W=D(W,V,Y,X,C[P+7],l,4139469664);X=D(X,W,V,Y,C[P+10],j,3200236656);Y=D(Y,X,W,V,C[P+13],o,681279174);V=D(V,Y,X,W,C[P+0],m,3936430074);W=D(W,V,Y,X,C[P+3],l,3572445317);X=D(X,W,V,Y,C[P+6],j,76029189);Y=D(Y,X,W,V,C[P+9],o,3654602809);V=D(V,Y,X,W,C[P+12],m,3873151461);W=D(W,V,Y,X,C[P+15],l,530742520);X=D(X,W,V,Y,C[P+2],j,3299628645);Y=t(Y,X,W,V,C[P+0],U,4096336452);V=t(V,Y,X,W,C[P+7],T,1126891415);W=t(W,V,Y,X,C[P+14],R,2878612391);X=t(X,W,V,Y,C[P+5],O,4237533241);Y=t(Y,X,W,V,C[P+12],U,1700485571);V=t(V,Y,X,W,C[P+3],T,2399980690);W=t(W,V,Y,X,C[P+10],R,4293915773);X=t(X,W,V,Y,C[P+1],O,2240044497);Y=t(Y,X,W,V,C[P+8],U,1873313359);V=t(V,Y,X,W,C[P+15],T,4264355552);W=t(W,V,Y,X,C[P+6],R,2734768916);X=t(X,W,V,Y,C[P+13],O,1309151649);Y=t(Y,X,W,V,C[P+4],U,4149444226);V=t(V,Y,X,W,C[P+11],T,3174756917);W=t(W,V,Y,X,C[P+2],R,718787259);X=t(X,W,V,Y,C[P+9],O,3951481745);Y=K(Y,h);X=K(X,E);W=K(W,v);V=K(V,g)}var i=B(Y)+B(X)+B(W)+B(V);return i.toLowerCase()};
              
                          var url = 'http://www.gravatar.com/avatar/' + MD5(email) + ".jpg?s=200&r=g";
                          console.log(url);
                          return url;
                      }
              };
              });
              اگر زیرنویس‌ها دارای اشکال هستند می‌توانید در این قسمت فایل‌های اصلاح شده را مجدداً آپلود کنید.
              مطالب
              پشتیبانی آنلاین سایت با 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();