مطالب
افزونه rtlColResizable جهت تغییر سایز ستون‌های جدول‌های راست به چپ HTML
برای تغییر سایز ستون‌های جداول HTML با استفاده از ماوس، افزونه‌های زیادی تدارک دیده شده است که از جمله مطرح‌ترین آن‌ها می‌توان به colResizable اشاره کرد. حتی اگر از DataGrid‌های مطرح وب هم استفاده کرده باشید، اکثر آن‌ها از تغییر سایز ستون‌ها توسط کاربر پشتیبانی می‌کنند. اما مشکل بزرگی که در همه‌ی آن‌ها مشترک است  این است که فقط از چیدمان‌های چپ به راست پشتیبانی می‌کنند و به محض اینکه شما ساختار راست به چپ را به جدول مورد نظر اعمال کنید، عملکرد تغییر سایز ستون‌ها با اشکال مواجه می‌شود.
به همین جهت برای تغییر سایز ستون‌ها توسط کاربر، افزونه ای برای jQuery تدارک دیدم که با جداول معمول HTML که همان table‌ها هستند سازگار است و به راحتی به هر جدولی می‌توان آن را اعمال کرد.
 
کدهای افزونه‌ی rtlColResizable:
(function ($, undefined) {

    $.fn.extend({
        'rtlColResizable': function (options) {
            var defaults = {
                //Default values for the plugin's options here
            };

            options = $.extend(defaults, options);

            var isMouseButtonPressed;
            var $resizingElement = undefined;
            var resizingElementStartWidth;
            var mouseCursorStartX;
            var isCursorInResizingPosition;

            var addResizingCursorStyle = function ($element) {
                $element.css({
                    'cursor': 'col-resize',
                    'user-select': 'none',
                    '-o-user-select': 'none',
                    '-ms-user-select': 'none',
                    '-moz-user-select': 'none',
                    '-khtml-user-select': 'none',
                    '-webkit-user-select': 'none',
                });
            };

            var removeResizingCursorStyle = function ($element) {
                $element.css({
                    'cursor': 'default',
                    'user-select': 'text',
                    '-o-user-select': 'text',
                    '-ms-user-select': 'text',
                    '-moz-user-select': 'text',
                    '-khtml-user-select': 'text',
                    '-webkit-user-select': 'text',
                });
            };

            var canResize = function (e) {
                return (e.offsetX || e.clientX - $(e.target).offset().left) < 10;
            };

            return this.each(function () {

                var opts = options;
                var tableColumns = $(this).find('th');

                tableColumns.filter(':not(:last-child)').mousedown(function (e) {

                    $resizingElement = $(this);
                    isMouseButtonPressed = true;
                    mouseCursorStartX = e.pageX;
                    resizingElementStartWidth = $resizingElement.width();

                });

                tableColumns.mousemove(function (e) {

                    if (canResize(e)) {
                        addResizingCursorStyle($(e.target));
                        isCursorInResizingPosition = true;

                    } else if (!isMouseButtonPressed) {
                        removeResizingCursorStyle($(e.target));
                        isCursorInResizingPosition = false;
                    }

                    if (isCursorInResizingPosition && isMouseButtonPressed) {

                        $resizingElement.width(resizingElementStartWidth + (mouseCursorStartX - e.pageX));
                    }

                });

                $(document).mouseup(function () {
                    if (isMouseButtonPressed) {
                        removeResizingCursorStyle($resizingElement);
                        isMouseButtonPressed = false;
                    }

                });

            });
        }
    });

})(jQuery);
 
نحوه‌ی استفاده:
این افزونه با تگ Table در HTML سازگار است. فقط تنها موردی که باید رعایت شود این است که در هنگام تعریف ساختار جدول، باید استاندارد تعریف ستون‌ها یا همان Header‌ها را رعایت کنید و از تگ‌های thead و th استفاده کنید.
نمونه ای از نحوه‌ی استفاده از آن را در کدهای زیر می‌بینید:
<!DOCTYPE html>
<html>
<head>
    <title>rtlColResizable Sample</title>
</head>
<body>
    <table id="myTable">
        <thead>
            <tr>
                <th>ستون1</th> <!-- نام ستون‌ها را حتما در این تگ باید تعریف کنید -->
                <th>ستون2</th>
                <th>ستون3</th>
            </tr>
        </thead>
        <tr>
            <td>داده مربوط به ستون 1</td>
            <td>داده مربوط به ستون 2 </td>
            <td>داده مربوط به ستون 3</td>
        </tr>
    </table>

    <script type="text/javascript" src="jquery-1.9.1.min.js"></script>
    <script type="text/javascript" src="jquery.rtlColResizable.js"></script>
    <script>

        $('table#myTable').rtlColResizable();

    </script>
</body>
</html>
     
دریافت نمونه کدی از نحوه‌ی استفاده از این افزونه
 
مطالب
پشتیبانی آنلاین سایت با 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();

مطالب
شروع به کار با Ember.js
Ember.js کتابخانه‌ای است جهت ساده سازی تولید برنامه‌های تک صفحه‌ای وب. برنامه‌هایی که شبیه به برنامه‌های دسکتاپ در مرورگر کاربر عمل می‌کنند. دو برنامه نویس اصلی آن Yehuda Katz که عضو اصلی تیم‌های jQuery و Ruby on Rails است و Tom Dale که ابتدا SproutCore را به وجود آورد و بعدها به Ember.js تغییر نام یافت، هستند.

منابع اصلی Ember.js

پیش از شروع به بحث نیاز است با تعدادی از سایت‌های اصلی مرتبط با Ember.js آشنا شد:
سایت اصلی: http://emberjs.com
مخزن کدهای آن: https://github.com/emberjs
انجمن اختصاصی پرسش و پاسخ: http://discuss.emberjs.com
موتور قالب‌های آن: http://handlebarsjs.com
لیست منابع مطالعاتی مرتبط مانند ویدیوهای آموزشی و لیست مقالات موجود: http://emberwatch.com
و بسته‌ی نیوگت آن: https://www.nuget.org/packages/EmberJS



مفاهیم پایه‌ای Ember.js

شیء Application
 App = Ember.Application.create();
یک برنامه‌ی Ember.js با تعریف وهله‌ای از شیء Application آن آغاز می‌شود. با اینکار به صورت خودکار رویدادگردان‌هایی به صفحه اضافه می‌شوند. کامپوننت‌های پیش فرض آن ایجاد شده و همچنین قالب اصلی برنامه رندر می‌شود.

مسیر یابی
با مرور قسمت‌های مختلف برنامه توسط کاربر، نیاز است حالات برنامه را مدیریت کرد؛ اینجا است که کار قسمت مسیریابی شروع می‌شود. مسیریابی، منابع مورد نیاز جهت آدرس‌های مشخصی را تامین می‌کند.
App.Router.map(function() {
    this.resource('accounts'); // takes us to /accounts
    this.resource('gallery'); // takes us to /gallery
});
در اینجا نحوه‌ی تعریف آغازین مسیریابی Ember.js را مشاهده می‌کنید که توسط متد resource آن مسیرهای قابل ارائه توسط برنامه مشخص می‌شوند.
به این ترتیب مسیرهای accounts/ و gallery/ قابل پردازش خواهند شد.

این مسیرها، تو در تو نیز می‌توانند باشند. برای مثال:
App.Router.map(function() {
    this.resource('news', function() {
        this.resource('images', function () { // takes us to /news/images
            this.route('add');// takes us to /news/images/add
        });
    });
});
به این ترتیب نحوه‌ی تعریف مسیریابی آدرس news/images/add را مشاهده می‌کنید. همچنین در این مثال از دو متد resource و route استفاده شده‌است. از متد resource برای حالت تعریف اسامی استفاده کنید و از متد route برای تعریف افعال و تغییر دهنده‌ها. برای نمونه در اینجا فعل افزودن تصاویر با متد route مشخص شده‌است.


مدل‌ها
مدل‌ها همان اشیایی هستند که برنامه مورد استفاده قرار می‌دهد و می‌توانند یک آرایه‌ی ساده و یا اشیاء JSON دریافتی از وب سرور باشند.
حداقل به دو روش می‌توان مدل‌ها را تعریف کرد:
الف) با استفاده از افزونه‌ی Ember Data
ب) با کمک شیء Ember.Object
App.SiteLink = Ember.Object.extend({});
App.SiteLink.reopenClass({
    findAll: function() {
        var links = [];
        //… $.getJSON …

        return links;
    }
});
ابتدا یک زیرکلاس از Ember.Object به کمک متد extend ایجاد خواهد شد. سپس از متد توکار reopenClass برای توسعه‌ی API کمک خواهیم گرفت.
در ادامه متد دلخواهی را ایجاد کرده و برای مثال آرایه‌ای از اشیاء دلخواه جاوا اسکریپتی را بازگشت خواهیم داد.
پس از تعریف مدل، نیاز است آن‌را به سیستم مسیریابی معرفی کرد:
App.GalleryRoute = Ember.Route.extend({
    model: function() {
        return App.SiteLink.findAll();
    }
});
به این ترتیب زمانیکه کاربر به آدرس gallery/ مراجعه می‌کند، دسترسی به model وجود خواهد داشت. در اینجا model یک واژه‌ی کلیدی است.


کنترلرها
کنترلرها جهت ارائه‌ی اطلاعات مدل‌ها به View و قالب برنامه تعریف می‌شوند. در اینجا همیشه باید بخاطر داشت که model تامین کننده‌ی اطلاعات است. کنترلر جهت در معرض دید قرار دادن این اطلاعات، به View برنامه کاربرد دارد و مدل‌ها هیچ اطلاعی از وجود کنترلرها ندارند.
کنترلرها علاوه بر اطلاعات model، می‌توانند حاوی یک سری خواص و اشیاء صرفا نمایشی که قرار نیست در بانک اطلاعاتی ذخیره شوند نیز باشند.
در Ember.js قالب‌ها (templates) اطلاعات خود را از کنترلر دریافت می‌کنند. کنترلرها اطلاعات مدل را به همراه سایر خواص نمایشی مورد نیاز در اختیار View و قالب‌های برنامه قرار می‌دهند.


برای تعریف یک کنترلر می‌توان درون شیء مسیریابی، با تعریف متد setupController شروع کرد:
App.GalleryRoute = Ember.Route.extend({
    setupController: function(controller) {
        controller.set('content', ['red', 'yellow', 'blue']);
    }
});
در این مثال یک خاصیت دلخواه به نام content تعریف و سپس آرایه‌ای به آن انتساب داده شده‌است.

روش دوم تعریف کنترلرها با ایجاد یک زیر کلاس از شیء Ember.Controller انجام می‌شود:
App.GalleryController = Ember.Controller.extend({
    search: '',
    content: ['red', 'yellow', 'blue'],
    query: function() {
        var data = this.get('search');
        this.transitionToRoute('search', { query: data });
    }
});


قالب‌ها یا templates
قالب‌ها قسمت‌های اصلی رابط کاربری را تشکیل خواهند داد. در اینجا از کتابخانه‌ای به نام handlebars برای تهیه قالب‌های سمت کاربر کمک گرفته می‌شود.
<script type="text/x-handlebars" data-template-name="sayhello">
    Hello,
    <strong>{{firstName}} {{lastName}}</strong>!
</script>
این قالب‌ها توسط تگ اسکریپت تعریف شده و نوع آن‌ها text/x-handlebars مشخص می‌شود. به این ترتیب Ember.js، این قسمت از صفحه را یافته و عبارات داخل {{}} را با مقادیر دریافتی از کنترلر جایگزین می‌کند.
<script type="text/x-handlebars" data-template-name="sayhello">
    Hello,
    <strong>{{firstName}} {{lastName}}</strong>!
 
    {{#if person}}
    Welcome back,
    <strong>{{person.firstName}} {{person.lastName}}</strong>!
    {{/if}}
 
    <ul>
        {{#each friend in friends}}
        <li>
            {{friend.name}}
        </li>
        {{/each}}
    </ul>
 
    <img {{bindAttr src="link.url" }} />
    {{#linkTo ''about}}About{{/linkTo}}
</script>
در این مثال نحوه‌ی تعریف عبارات شرطی و یا یک حلقه را نیز مشاهده می‌کنید. همچنین امکان اتصال به ویژگی‌هایی مانند src یک تصویر و یا ایجاد لینک‌ها را نیز دارا است.
بهترین مرجع آشنایی با ریز جزئیات کتابخانه‌ی handlebars، مراجعه به سایت اصلی آن است.


قواعد پیش فرض نامگذاری در Ember.js
اگر به مثال‌های فوق دقت کرده باشید، خواصی مانند GalleryController و یا GalleryRoute به شیء App اضافه شده‌اند. این نوع نامگذاری‌ها در ember.js بر اساس روش convention over configuration کار می‌کنند. برای نمونه اگر مسیریابی خاصی را به نحو ذیل تعریف کردید:
 this.resource('employees');
شیء مسیریابی آن App.EmployeesRoute
کنترلر آن App.EmployeesController
مدل آن App.Employee
View آن App.EmployeesView
و قالب آن employees
بهتر است تعریف شوند. به عبارتی اگر اینگونه تعریف شوند، به صورت خودکار توسط Ember.js یافت شده و هر کدام با مسئولیت‌های خاص مرتبط با آن‌ها پردازش می‌شوند و همچنین ارتباطات بین آن‌ها به صورت خودکار برقرار خواهد شد. به این ترتیب برنامه نظم بهتری خواهد یافت. با یک نگاه می‌توان قسمت‌های مختلف را تشخیص داد و همچنین کدنویسی پردازش و اتصال قسمت‌های مختلف برنامه نیز به شدت کاهش می‌یابد.


تهیه‌ی اولین برنامه‌ی Ember.js
تا اینجا نگاهی مقدماتی داشتیم به اجزای تشکیل دهنده‌ی هسته‌ی Ember.js. در ادامه مثال ساده‌ای را جهت نمایش ساختار ابتدایی یک برنامه‌ی Ember.js، بررسی خواهیم کرد.
بسته‌ی Ember.js را همانطور که در قسمت منابع اصلی آن در ابتدای بحث عنوان شد، می‌توانید از سایت و یا مخزن کد آن دریافت کنید و یا اگر از VS.NET استفاده می‌کنید، تنها کافی است دستور ذیل را صادر نمائید:
 PM> Install-Package EmberJS
پس از اضافه شدن فایل‌های js آن به پوشه‌ی Scripts برنامه، در همان پوشه‌، فایل جدید Scripts\app.js را نیز اضافه کنید. از آن برای افزودن تعاریف کدهای Ember.js استفاده خواهیم کرد.
در این حالت ترتیب تعریف اسکریپت‌های مورد نیاز صفحه به صورت ذیل خواهند بود:
<script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script>
<script src="Scripts/handlebars.js" type="text/javascript"></script>
<script src="Scripts/ember.js" type="text/javascript"></script>
<script src="Scripts/app.js" type="text/javascript"></script>
کدهای ابتدایی فایل app.js جهت وهله سازی شیء Application و سپس تعریف مسیریابی صفحه‌ی index بر اساس روش convention over configuration به همراه تعریف یک کنترلر و افزودن متغیری به نام content به آن که با یک آرایه مقدار دهی شده‌است:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
    setupController:function(controller) {
        controller.set('content', ['red', 'yellow', 'blue']);
    }
});
باید دقت داشت که تعریف مقدماتی Ember.Application.create به همراه یک سری تنظیمات پیش فرض نیز هست. برای مثال مسیریابی index به صورت خودکار به نحو ذیل توسط آن تعریف خواهد شد و نیازی به تعریف مجدد آن نیست:
App.Router.map(function() {
    this.resource('application'); 
    this.resource('index');
});
سپس برای اتصال این کنترلر به یک template خواهیم داشت:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script>
    <script src="Scripts/handlebars.js" type="text/javascript"></script>
    <script src="Scripts/ember.js" type="text/javascript"></script>
    <script src="Scripts/app.js" type="text/javascript"></script>
</head>
<body>
    <script type="text/x-handlebars" data-template-name="index">
        Hello,
        <strong>Welcome to Ember.js</strong>!
        <ul>
            {{#each item in content}}
            <li>
                {{item}}
            </li>
            {{/each}}
        </ul>
    </script>
</body>
</html>
توسط اسکریپتی از نوع text/x-handlebars، اطلاعات آرایه content دریافت و در طی یک حلقه در صفحه نمایش داده خواهد شد.
مقدار data-template-name در اینجا مهم است. اگر آن‌را به هر نام دیگری بجز index تنظیم کنید، منبع دریافت اطلاعات آن مشخص نخواهد بود. نام index در اینجا به معنای اتصال این قالب به اطلاعات ارائه شده توسط کنترلر index است.

تا همینجا اگر برنامه را اجرا کنید، به خوبی کار خواهد کرد. نکته‌ی دیگری که در مورد قالب‌های Ember.js قابل توجه هستند، قالب پیش فرض application است. با تعریف Ember.Application.create یک چنین قالبی نیز به ابتدای هر صفحه به صورت خودکار اضافه خواهد شد:
<body>
    <script type="text/x-handlebars" data-template-name="application">
        <h1>Header</h1>
        {{outlet}}
    </script>
outlet واژه‌‌ای است کلیدی که سبب رندر سایر قالب‌های تعریف شده در صفحه می‌گردد. مقدار data-template-name آن نیز به application تنظیم شده‌است (اگر این مقدار ذکر نگردد نیز به صورت خودکار از application استفاده می‌شود). برای مثال اگر بخواهید به تمام قالب‌های رندر شده در صفحات مختلف، مقدار ثابتی را اضافه کنید (مانند هدر یا منو)، می‌توان قالب application را به صورت دستی به نحو فوق اضافه کرد و آن‌را سفارشی سازی نمود.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS01.zip
مطالب
مدیریت کلیدهای کیبرد در جاوا اسکریپت
با پیشرفت بسترهای موجود در زمینه شبکه و اینترنت، گرایش به استفاده از اپلیکیشنهای تحت وب روز به روز بیشتر میشود. با گسترش این برنامه‌ها نیازها و درنتیجه ابزارهای موجود توسعه پیدا می‌کنند. درحال حاضر ابزارها و نیز محیطهای توسعه مختلفی برای تولید این اپلیکیشنها وجود دارد. به دلیل نوع رابط کاربری موجود در این برنامه‌ها (اکثراً مرورگرهای وب مثل اینترنت اکسپلورر، گوگل کروم، فایرفاکس و ...) استفاده از زبانهای سمت کلاینت (مثل جاوا اسکریپت که در تمامی مرورگرهای مدرن پشتیبانی کاملی از آن میشود) جایگاه ویژه ای در این نوع برنامه‌ها دارد. درضمن وقتی صحبت از اپلیکیشن به میان می‌آید استفاده از کلیدهای میانبر کیبرد برای راحتی کار کاربران کاربرد ویژه ای دارد. اما متاسفانه زبان جاوا اسکریپت به دلیل محدودیتهایی منطقی موجود، پشتیبانی مناسبی از رویدادهای کیبرد ندارد و مشکل تفاوتها و تناقضات میان سخت افزارها، سیستم عامل‌ها و مرورگرها هم به این مسئله بیشتر دامن میزند. در مطلب جاری هدف این است تا آشنایی مقدماتی با این مبحث فراهم شود.
رویدادهای کیبرد
در جاوا اسکریپت سه رویداد زیر برای کلیدهای کیبرد وجود دارد (به ترتیب زمان رخ دادن):
keydown: زمانی که یک کلید فشرده می‌شود.
keypress: زمانی که یک کلید کاراکتری فشرده می‌شود.
keyup: زمانی که یک کلیدِ فشرده شده، رها می‌شود.
یک تفاوت اساسی میان رویدادهای keydown و keypress در جاوا اسکریپت وجود دارد: رویداد keydown پس از فشردن هر کلیدی روی کیبرد رخ میدهد و یک کد مخصوص آن کلید (scan code ^) را ارائه میدهد. اما رویداد keypress که بعد از keydown رخ میدهد کد کاراکتر آن کلید (char code) را ارائه میدهد، بنابراین تنها برای کلیدهای کاراکتری بدرستی کار میکند. برای درک بهتر کد زیر را در یک فایل html ذخیره کرده و در مرورگرهای مختلف آزمایش کنید:
<html>
<body>
  <div>
    Prevent default:
    <input type="checkbox" id="keydownStop" value="1" />
    keydown&nbsp;&nbsp;&nbsp;
    <input type="checkbox" id="keypressStop" value="1" />
    keypress&nbsp;&nbsp;&nbsp;
    <input type="checkbox" id="keyupStop" value="1" />
    keyup
  </div>
  Ignore:
  <input type="checkbox" id="keydownIgnore" value="1" />
  keydown &nbsp;&nbsp;&nbsp;
  <input type="checkbox" id="keypressIgnore" value="1" />
  keypress &nbsp;&nbsp;&nbsp;
  <input type="checkbox" id="keyupIgnore" value="1" />
  keyup
  <div>
    Focus on the input below and press any key.
  </div>
  <div>
    <input type="text" style=" width: 600px" id="keyInput" />
  </div>
  Log:
  <div>
    <textarea id="keyLogger" rows="18" onfocus="this.blur()" style="width: 600px; border: 1px solid black"></textarea>
  </div>
  <input type="button" value="Clear" onclick="clearLog()" />
  <script type="text/javascript">
    document.getElementById('keyInput').onkeydown = keyHandler;
    document.getElementById('keyInput').onkeyup = keyHandler;
    document.getElementById('keyInput').onkeypress = keyHandler;
    document.getElementById('keyInput').focus();
    function keyHandler(e) {
      e = e || window.event;
      if (document.getElementById(e.type + 'Ignore').checked) return;
      var evt = e.type;
      while (evt.length < 10) evt += ' ' +
        log(evt +
          ' keyCode=' + e.keyCode +
          ' which=' + e.which +
          ' charCode=' + e.charCode +
          ' char=' + String.fromCharCode(e.keyCode || e.charCode) +
          (e.shiftKey ? ' +shift' : '') +
          (e.ctrlKey ? ' +ctrl' : '') +
          (e.altKey ? ' +alt' : '') +
          (e.metaKey ? ' +meta' : ''));
      if (document.getElementById(e.type + 'Stop').checked) {
        e.preventDefault ? e.preventDefault() : (e.returnValue = false);
      }
    }
    function clearLog() {
      document.getElementById('keyLogger').value = '';
      document.getElementById('keyInput').focus();
    }
    function log(text) {
      var area = document.getElementById('keyLogger');
      area.value += text + '\n';
      area.scrollTop = area.scrollHeight;
    }
  </script>
</body>
</html>
نکته: برای جلوگیری از اجرای مرورگرها در حالت Quirks حتما از تگ doctype در ابتدای فایلهای html خود استفاده کنید. درغیراینصورت رفتارهای غیرمنتظره ای (مخصوصا در IE) مشاهده خواهید کرد. برای اجرای مرورگرها در حالت استاندارد html5 (بهترین حالت در حال حاضر) میتوانید از تگ زیر استفاده کنید:
<!doctype html>
دقت کنید که قبل از این خط هیچ چیز دیگری نوشته نشود وگرنه در IE از آن صرفنظر میشود!
یا اینکه در IE با استفاده از developer tools (دکمه F12) برای Document Mode گزینه ای غیر از Quirks mode (بهتر است از حالت IE9 یا بالاتر استفاده کنید) را انتخاب کنید.

برای کسب اطلاعات بیشتر راجع به doctypeهای مختلف و نیز حالت quirks میتوانید به ^ و ^ و ^ و ^ رجوع کنید. پیشنهاد میکنم که این منابع را حتما مطالعه کنید.
نکته: در کد بالا متد preventDefault در  -8 IE تعریف نشده است (درواقع در IE تنها در نسخه 9 تعریف شده است). همچنین استفاده از پراپرتی returnValue در فایرفاکس و IE9 کار نمیکند! از این خط کد برای جلوگیری از رفتار پیشفرض رویداد استفاده شده است. همانطور که در ادامه میخوانید راه حل ساده‌تری نیز برای اینکار وجود دارد.
متد String.fromCharCode برای نمایش کاراکتر کلید فشرده شده استفاده شده است. البته اگر کلید غیرکاراکتری فشرده شود ممکن است با نتایج غیرمنتظره ای روبرو شوید.
با استفاده از html تولیدی در مرورگرهای مختلف سعی کنید موارد زیر را آزمایش کنید:
کلیدهای کاراکتری چون a / | { 6 را  بفشارید. در این حالت رویدادهای keydown و سپس keypress رخ خواهند داد. پس از رها کردن کلیدها نیز رویداد keyup رخ میدهد.
یکی از کلیدهای غیرکاراکتری مثل ctrl یا alt را بفشارید. در این حالت تنها رویدادهای keydown و keyup رخ خواهند داد و خبری از رویداد keypress نیست.
نکته: مرورگرهای FireFox و Opera در مورد بیشتر کلیدهای غیرکاراکتری نیز رویداد keypress را صدا خواهند زد! مرورگر IE این رفتار را تنها در مورد کلید Esc نشان میدهد. همچنین در IE و Opera کلید PrtScr هیچ رویدادی را فرا نمیخواند. ظاهرا تنها مرورگر Chrome بدرستی عمل میکند. 
درحالت کلی فشردن کلیدهای غیرکاراکتری نباید رویداد keypress را فراخوانی کند.
بنابراین:
keydown و keyup برای همه کلیدها
keypress برای کلیدهای کاراکتری

پراپرتی‌های رویدادهای کیبرد
برخلاف نسخه‌های قدیمی مرورگرها که هرکدام راه و روش خودشان را برای تعامل با این رویدادها برگزیده بودند، امروزه تمامی مرورگرها تقریبا از یک روش استاندارد و مشترک برای اینکار استفاده میکنند. در تصاویر زیر تمام اجزای آبجکت KeyboardEvent با استفاده از کد زیر در مرورگرهای اپرا و کروم نشان داده شده است. استفاده از کد زیر در مرورگرهای فایرفاکس و IE نتایج جالبی مانند تصاویر زیر فراهم نمیکند!
    document.onkeydown = function (e) {
      e = e || event;
      console.log(e);
    }

همانطور که مشاهده میکنید تفاوتهایی بین مرورگرها در این آبجکت به چشم میخورد. برای کسب اطلاعات بیشتر در مورد این آبجکت و اجزای استاندارد آن در DOM Level 3 به اینجا مراجعه کنید. در ادامه به بررسی پراپرتی‌های مهم آرگومان این رویدادها (همان KeyboardEvent) میپردازیم.
keyCode
همان scan code کلید فشرده شده است. برای مثال اگر کلید a فشرده شود کاراکتر تولیدی ممکن است a یا A یا 'ش' (یا کاراکتری دیگر در زبانهای مختلف) باشد اما در تمامی حالات scan code مربوطه یا همان keyCode همیشه یکسان (65 برای کلید a) خواهد بود. این کد تنها به کلید فشرده شده بستگی دارد و نه به کاراکتر حاصله! البته در IE به هنگام رخ دادن رویداد keypress کد کاراکتر (همان char code) کلید فشرده شده در این پراپرتی قرار میگیرد!
در این بین میان مرورگرهای مختلف تفاوتهایی وجود دارد که با یک جستجو در اینترنت میتوان به تمامی این کدها دسترسی پیدا کرد. خواندن مقاله کامل JavaScript Madness: Keyboard Events نیز خالی از لطف نیست.
charCode
همان کد ASCII (یا کد UTF-16 برای کاراکترهای یونیکد. اطلاعات بیشتر ^ و ^) کاراکتر کلید فشرده شده است. ممکن است با keyCode برابر باشد. این پراپرتی در IE و Opera تعریف نشده است.
در عمل ممکن است keyCode و charCode در پلتفرمهای مختلف و حتی بین سیستم عامل‌های مختلف در یک سخت افزار نتایج متفاوتی ارائه دهند. بنابراین آزمودن هر مورد مشکوک قبل از ریلیز نهایی محصول میتواند مفید باشد.
which
یک پراپرتی نسبتا غیراستاندارد! است که ترکیبی از keyCode و charCode را برمیگرداند (اطلاعات بیشتر در ^ و ^). این پراپرتی در IE تعریف نشده است.
shiftKey, ctrlKey, altKey, metaKey 
پراپرتی هایی از نوع بولین که وضعیت کلیدهای Shift, Ctrl, Alt و Command (تنها در مک) را نشان میدهند. 
نکته: بدستن آوردن کد درست کاراکتر فشرده شده با توجه به وضعیت کلیدها و زبان انتخابی تنها با استفاده از رویداد keypress امکان پذیر است. با توجه به تفاوتهایی که بین مرورگرهای مختلف وجود دارد برای یافتن کاراکتر فشرده شده در رویداد keypress استفاده از متد زیر توصیه میشود:
function getCharacter(event) {
  if (event.which == null)
     return String.fromCharCode(event.keyCode);    // IE
  else if (event.which != 0 && event.charCode != 0)
     return String.fromCharCode(event.which);  // All others
  return null; // special key
}
توضیحات بیشتر و کاملتر این مبحث را میتوانید از Document Object Model (DOM) Level 3 Events Specification که آخرین نسخه آن در زمان تهیه این مطلب در تاریخ 14 June 2012 انتشار یافته تهیه کنید. بطور ویژه برای مبحث رویدادهای کیبرد (^) اطلاعات بسیار بیشتری در دسترس است.
نکته: با توجه به اطلاعاتی که در سند فوق وجود دارد، به دلیل ماهیت همزمانی (Sync) رویدادهای کیبرد تا زمانی که تمام عملیات موجود در متد تعیین شده برای این رویدادها انجام نشود، رویداد بعدی (با توجه به ترتیبی که در ابتدای این مطلب آورده شده است) رخ نخواهد داد. برای تست این موضوع قطعه کد زیر را آماده کردم:
<html>
<body>
  <input id="inputText" type="text"  />
  <script>
    document.onkeydown = keyDown;
    document.onkeypress = keyPress;
    document.onkeyup = keyUp;
    function keyDown(e) {
      var input = document.getElementById('inputText');
      input.value = 'keyDown started ...';
      input.disabled = true;
      var j = 0;
      for (var i = 0; i < 999999999; i++) {
        j = i - j;
      }
      console.log(j);
      //alert('keyDown');
      input.value = 'keyDown finished.';
      input.disabled = false;
    }
    function keyPress(e) {
      alert('keyPress');
      //console.log('keyPress');
    } function keyUp(e) {
      alert('keyUp');
      //console.log('keyUp');
    }
  </script>
</body>
</html>
اگر کد بالا در مرورگرهای مختلف امتحان کنید مشاهده میکنید که انجام عملیات سنگین در رویدادهای کیبرد موجب ایجاد وقفه در فراخوانی سایر رویدادها میشود.
با اجرای کد فوق در مرورگرهای مختلف نکات جالب زیر بدست آمد:
- در IE و کروم نمایش یک alert موجب از دست دادن فوکس document شده و بنابراین رویدادهای کیبرد بعد از نمایش alert کار نخواهند کرد! مثلا اگر در keydown یک alert نمایش داده شود چون رویداد keyup بر روی پنجره alert رخ میدهد بنابراین keyup فراخوانی نمیشود ولی چون رویداد keypress با keydown همزمان است این اتفاق برای keypress نمی‌افتد. این مشکل در فایرفاکس پیش نمی‌آید. در اپرا در این حالت رویداد keypress هم رخ نمیدهد! البته رفتار IE در اجرای کد فوق کمی غیرمنتظره‌تر است و ظاهرا رویداد keypress هم رخ نمیدهد.
- در اجرای کد فوق در FireFox ظاهرا alert مربوط به keyup قبل از keypress نمایش داده میشود. البته اگر به جای alert از console.log (البته نیاز به نصب Firebug است) استفاده شود این به هم خوردگی ترتیب رویدادها وجود ندارد.
- در مرورگر Opera پس از فشردن کلید enter در نوار آدرس و پس از بارگذاری صفحه، فوکس بلافاصله به عنصر document سپرده میشود طوریکه که رویداد keyup کلید enter در document بارگذاری شده فراخوانی میشود و درصورت سرعت بالای بارگذاری صفحه، کدهای مروبوط به این رویداد اجرا میشوند. در سایر مرورگرها این مورد مشاهده نشد. 
- ظاهرا در تمام مرورگرها به غیر Opera کدهای جاوا اسکریپ در ثرد UI اجرا شده و موجب قفل شدن document میشود. بنابراین وسط اجرای کدهای سنگین نمیتوان مثلا خواص عناصر UI را تغییر داد. درواقع مرورگر اپرا برخلاف سایر مرورگرها، رفتار ویژه ای در برخورد با جاوا اسکریپت و رویدادها و تغییرات عناصر DOM دارد. برای کسب اطلاعات بیشتر در این زمینه به اینجا مراجعه کنید. درضمن این مبحث کمی پیچیده‌تر از آن است که به نظر می‌آید(^). برای بررسی بیشتر میتوانید کد زیر را در مرورگرهای مختلف آزمایش کنید:
<html>
<head>
  <script type="text/javascript">
    function process() {
      var above = 0, below = 0;
      for (var i = 0; i < 200000; i++) {
        if (Math.random() * 2 > 1) {
          above++;
        }
        else {
          below++;
        }
      }
    }
    function test() {
      var result1 = document.getElementById('log');
      var start = new Date().getTime();
      console.log('start');
      for (var i = 0; i < 200; i++) {
        result1.value = 'time=' + (new Date().getTime() - start) + ' [i=' + i + ']';
        process();
      }
      result1.value = 'time=' + (new Date().getTime() - start) + ' [done]';
      console.log('end');
    }
    window.onload = test;
  </script>
</head>
<body>
  <input id='log' />
</body>
</html>
اگر کد فوق را در مرورگرهایی غیر از اپرا اجرا کنید میبینید که تنها نتیجه نهایی نمایش داده میشود و فرایندهای میانی درون حلقه نمیتوانند تغییری در محتوای UI ایجاد کنند. طبق انتظار کروم از بقیه بسیار سریعتر بوده و سپس IE و پس از آن فایرفاکس قرار دارد. اما در مورد Opera وضع کاملا فرق میکند و به دلیل به روز رسانی همزمان UI عملیات بسیار بسیار کندتر از بقیه مرورگرها به اتمام میرسد.
نکته: حالات استثنایی دیگری هم در اجرای کدهای مشابه در مرورگرهای مختلف پیش می‌آید که به دلیل پیچیده کردن بیش از حد بحث آورده نشده اند. فقط ذکر این نکته الزامی است که درحال حاضر میزان تفاوت رفتار مرورگرهای مختلف دربرخورد با کدهای یکسان قابل ملاحظه است. بنابراین در هنگام توسعه سعی کنید حداقل در این چهار مرورگر معروف آزمایشات خود را به سرانجام برسانید.
و در ادامه چند مثال ...

غیرفعال کردن ورودی کاربر
برای اینکار فقط کافی است در رویدادهای keydown یا keypress مقدار false برگشت داده شود. البته در مروگر Opera برخی از کلیدها از این رفتار پیروی نمیکنند. مثل کاراکترهای '`' و '+' و '=' که درصورت برگشت false در رویداد keydown باز هم رفتار پیش فرض را از خود نشان میدهد. اما برگرداندن مقدار false در رویداد keypress این مشکل را حل میکند(این موارد در نسخه 11.51 تست شدند). میتوانید با استفاده از کد زیر در تمام مرورگرها این موارد را آزمایش کنید:
<input onkeydown="return false" />
<input onkeypress="return false" /> 

تبدیل به حروف بزرگ
document.getElementById('myInputText').onkeypress = function (e) {
  var char = getCharacter(e || window.event);
  if (!char) return; // special key
  this.value += char.toUpperCase();
  return false; // برای اینکه کاراکتر اضافی نمایش داده نشود
}
در کد فوق متد getCharacter در بالا در قسمت پراپرتی‌های آرگومان رویداد نشان داده شده است. کد فوق چندان کامل نیست و همیشه کاراکتر فشرده شده را بدون توجه به موقعیت کرسر کیبرد در انتهای متن قرار میدهد.
نکته: استفاده از عبارتی چون e || window.event به این دلیل است که در مرورگر IE آرگومان رویداد (همان e که به آن implicit event object نیز میگویند) به متد مربوطه ارسال نمیشود (تا نسخه 9 که تست کردم مسئله به همین صورت است) در عوض پراپرتی‌های این آرگومان از طریق window.event (یا همان event که به آن explicit event object نیز میگویند) در دسترس هستند. این فیلد در واقع آرگومان آخرین رویداد رخ داده در پنجره جاری را در خود ذخیره میکند. اما در سایر مرورگرها به صورت استاندارد مقدار این آرگومان به عنوان پارامتر رویداد به متد مربوطه ارسال میشود. جالب است که بدانید مرورگرهای Opera و Chrome از هر دو روش پشتیبانی میکنند. مرورگر فایرفاکس تنها از ارسال آرگومان رویداد به متد مربوطه پشتیبانی میکند.
عبارت e ||window.event درواقع شکل دیگر عبارت زیر است:
e ? e : window.event;
در جاوا اسکریپت اگر عملگر مقایسه ای در عبارت مقایسه آورده نشود مقدار عبارت با false مقایسه میشود. این مقایسه از نوع abstract است (در ادامه این مطلب توضیح داده شده است). در جاوا اسکریپت 0 و رشته خالی و null و undefined و امثال اینها در مقایسه abstract برابر false درنظر گرفته میشوند. بنابراین در عبارت مقایسه بالا اگر مقدار e مثلا undefined (در IE) باشد مقدار window.event بازگشت داده میشود و درغیراینصورت خود e برگشت داده میشود.

تنها عدد
document.getElementById('numberInputText').onkeypress = function (e) {
  e = e || window.event;
  var chr = getCharacter(e);
  if (!isNumeric(chr) && chr !== null) return false;
}
function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}
function isNumber(val) {
  return val !== "NaN" && (+val) + '' === val + ''
}
در کد بالا متد isNumeric از اینجا گرفته شده است. متد isNumeric جی کوئری (که از نسخه 1.7 اضافه شده است) هم دقیقا از این روش استفاده میکند.
متد دوم یعنی  isNumber (که در کد فوق از آن استفاده نشده است) روش دیگری را برای اطمینان از مقدار عددی بودن استفاده میکند. در این روش درصورتیکه val یک مقدار عددی نباشد val+ برابر NaN میشود که در نهایت عبارت مقایسه ای مقدار false را برمیگرداند. البته به غیر از خود مقدار NaN که در شرط اول مورد بررسی قرار گرفته است. برای کسب اطلاعات بیشتر در مورد رفتار نسبتا عجیب جاوا اسکریپت با NaN و متد isNaN به اینجا سر بزنید.
نکته: تفاوت == با === (یا =! و ==!) - در جاوا اسکریپت دو روش برای مقایسه مقادیر متغیرها وجود دارد. اولی (== یا =!) مقایسه را با تبدل نوع داده‌ها انجام میدهد (که اصطلاحا به آن type-converting equality comparison یا abstract comparison میگویند) و دومی (=== یا ==!) مقایسه را با مقادیر واقعی و بدون تبدیل نوع داده انجام میدهد (که به آن equality without type coercion یا strict equality comparison گفته میشود). در واقع در مقایسه strict تنها وقتی که دو متغیر از یک نوع باشند ممکن است مقدار true برگشت داده شود. برای روشنتر شدن مطلب به مثالهای زیر توجه کنید (^):
0==false   // true
0===false  // false, because they are of a different type
1=="1"     // true, auto type coercion
1==="1"    // false, because they are of a different type 
توضیحات بهتری در اینجا آورده شده است.
برای کسب اطلاعات کاملتر میتوانید به ECMAScript Language Specification و قسمت مقایسه strict و abstract مراجعه کنید. (رابطه‌ها و تفاوت‌های میان ECMAScript و JavaScript و JScript و ActionScript در اینجا آورده شده است.)

کار با Scan Code و Char Code
همانطور که قبلا هم اشاره شد میان کد کاراکتر و کلید فشرده شده بر روی کیبرد تفاوت وجود دارد. برای بهره برداری از کلیدهای میانبر در صفحات وب به کد کلید فشرده شده (همان scan code) نیاز است و نه به کد کاراکتر آن. همچنین کلیدهای ویژه و غیرکاراکتری دارای کد کاراکتر نیستند. بیشتر مرورگرها رویداد Keypress را برای کلیدهای غیرکاراکتری فرا نمیخوانند. بنابراین برای این موارد رویدادهای keydown و keyup مفید هستند. امروز تمام مرورگرها از جدول کدهای یکسانی برای scan codeها استفاده میکنند که نمونه‌های آن را میتوانید در ^ و ^ و ^ مشاهده کنید. نکته ای که باید درباره پراپرتی keyCode یادآوری شود این است که جدای از وضعیت کیبرد (مثل زبان یا موقعیت کلید capsLock) در تمام حالات در ازای فشردن شده یک کلید خاص (یا ترکیبی از کلیدهای کنترلی با سایر کلیدها) همواره باید یک کد یکسان برگشت داده شود.
پس یک charCode کد یونیکد کاراکتر کلید فشرده شده است که تنها در رویداد keypress در دسترس است. یک keyCode کد خود کلید فشرده شده یا همان scan code است که در رویدادهای keydown و keyup در دسترس است.
نکته: برای تمام کلیدهای الفبایی-عددی scan code با char code یکی است. مثلا برای حروف الفبایی scan code یک کلید با کد ASCII حرف انگلیسی بزرگ آن کلید برابر است.
بنابراین برای بررسی کلید ترکیبی ctrl + a میتوان از کد زیر در رویداد keydown استفاده کرد:
e.ctrlKey && e.keyCode == 'A'.charCodeAt(0)
و فرقی نمیکند که کاراکتر حاصله 'a' یا 'A' یا 'ش' باشد. 
نکته: برای تمام کلیدهای کیبرد به غیر از ';' و '=' و '-' تمام مرورگرها از کدهای یکسانی استفاده میکنند. میتوانید این کلیدها را با استفاده از قطعه کد اول این مطلب در مرورگرهای مختلف آزمایش کنید.
نکته: با برگشت مقدار false در رویداد keydown یا keypress رفتار پیشفرض کلید یا ترکیب کلیدهای مربوطه غیرفعال میشود. البته به غیر از عملیاتهای سطح سیستم عامل (مثل alt+F4).
چند مثال دیگر در ادامه ...

جابجایی تصویر
کدهای زیر را در یک فایل html ذخیره کرده و توسط یک مرورگر فایل حاصله را باز کنید.
<html>
<body>
  <div id="dotnettips" style="width: 35px; height: 35px; background-image: url(https://www.dntips.ir/favicon.ico);
    position: absolute; left: 10px; top: 10px;" tabindex="0">
  </div>
  <script>
    document.getElementById('dotnettips').onkeydown = function (e) {
      e = e || event;
      switch (e.keyCode) {
        case 37: // left
          this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px';
          return false;
        case 38: // up
          this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px';
          return false;
        case 39: // right
          this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px';
          return false;
        case 40: // down
          this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px';
          return false;
      }
    }
  </script>
</body>
</html>
بر روی تصویر کلید کرده (یا با استفاده از Tab فوکس را روی تصویر قرار دهید) و با استفاده از کلیدهای مکان نما (Arrow keys) سعی کنید تصویر را در صفحه جابجا کنید.
در کد فوق برای جلوگیری از رفتار پیش فرض کلیدهای مکان نما (جابجایی محتوای صفحه در صورت وجود اسکرول) مقدار false برگشت داده شده است.
نکته: استفاده از خاصیت tabindex برای امکان فوکس بر روی div اجباری است.
نکته: استفاده از event به جای window.event تا زمانی که یک متغیر در اسکوپ جاری با نام event وجود نداشته باشد مشکلی ایجاد نمیکند.

Caps Lock
متاسفانه راه مستقیمی برای دریافت وضعیت کلید caps lock در جاوا اسکریپت وجود ندارد. ظاهرا تنها راه حل موجود برای این مسئله بررسی کد کاراکتر کلید فشرده شده در رویداد keypress و بررسی آن با توجه به وضعیت پراپرتی shift این رویداد است. بدین ترتیب که اگر کد کاراکتر مربوط به حروف بزرگ بوده درحالیکه کلید شیفت نگه داشته نشده است بنابراین کلید Caps Lock روشن است و بالعکس. البته با ترکیب این روش و نیز رصد scan code کلید Caps Lock (کد آن برابر 20 است) در رویداد keydown میتوان وضعیت بهتری پدید آورد. قطعه کد زیر برای اینکار است. در این کد از یک متغیر گلوبال برای نگهداری وضعیت دکمه caps lock استفاده میشود.
<html>
<body>
  <input id="inputText" type="text" />
  <div id="divCapsLock" style="color: Red; display: none;">Caps Lock is ON!</div>
  <script>
    var capsLock = null;
    document.onkeypress = keyPress;
    document.onkeydown = keyDown;
    function keyDown(e) {
      e = e || event;
      capsLock = (e.keyCode == 20 && capsLock !== null) ? !capsLock : capsLock;
      document.getElementById('divCapsLock').style.display = capsLock ? 'block' : 'none';
    }
    function keyPress(e) {
      if (capsLock != null) return;
      e = e || window.event;
      var charCode = e.charCode || e.keyCode;
      capsLock = (charCode >= 97 && charCode <= 122 && e.shiftKey) || (charCode >= 65 && charCode <= 90 && !e.shiftKey);
      document.getElementById('divCapsLock').style.display = capsLock ? 'block' : 'none';
    }
  </script>
</body>
</html>
در متد رویداد kepress تکست باکس ابتدا بررسی میشود که آیا قبلا متغیر گلوبال capsLock مقداری غیرنال دارد. اگر مقدار غیرنال داشته باشد دیگر نیازی نیست تا کد کاراکتر کلید بررسی شود زیرا وضعیت جاری کلید معلوم است. بنابراین در ادامه کار صرفه جویی میشود. دلیل استفاده از رویدادهای سطح docment به جای خود تکست باکس این است تا از کوچکترین فرصتها! برای تعیین وضعیت جاری کلید caps lock استفاده شود. یعنی بتوان پس از فشرده شدن اولین کلید کاراکتری بدون توجه به موقعیت فوکس در صفحه، این وضعیت را از حالت نامشخص خارج کرد.
نکته: در سلسله مراتب رویدادهای اجزای یک document همیشه ابتدا رویدادهای مربوط به عناصر فرزند فراخوانده میشود و رویدادهای مربوط به عناصر document و window در پایان صدا زده میشوند. برای آزمودن این مورد قطعه کد زیر را در مرورگرهای مختلف امتحان کنید:
<html>
<body>
  <div id='divInput'>
    <input id="inputText" type="text" />
  </div>
  <script>
    document.getElementById('inputText').onkeydown = inputKeyDown;
    document.getElementById('divInput').onkeydown = divKeyDown;
    document.onkeydown = documentKeyDown;
    window.onkeydown = windowKeyDown;
    function divKeyDown(e) {
      console.log('divKeyDown');
    }
    function inputKeyDown(e) {
      console.log('inputKeyDown');
    }
    function documentKeyDown(e) {
      console.log('documentKeyDown');
    }
    function windowKeyDown(e) {
      console.log('windowKeyDown');
    }
  </script>
</body>
</html>
نکته: اگر از تگ doctype استفاده نشود (همانطور که در ابتدای این مطلب اشاره شد)، در IE رویدادهای عنصر window فراخوانی نمیشوند.
درهرصورت برای مشخص کردن وضعیت کلید caps lock نیاز است تا کاربر ابتدا کلیدی را بفشارد و در حال حاضر روش و راه حل دیگری وجود ندارد! درضمن اگر صفحه کلیدی غیر از انگلیسی استفاده شود روش فوق جواب نخواهد داد و باید بررسی‌های بیشتری انجام شود. البته در مورد زیانهایی چون فارسی که روشن یا خاموش بودن caps lock تاثیری در کاراکتر حاصله ندارد کاری نمیتوان کرد و نمیتوان از وضعیت caps lock باخبر شد! هرچند در این موارد معمولا وضعیت این دکمه مهم نیست زیرا بیشترین کاربرد این گونه هشدارها در ورودی‌های رمز عبور است در صورتیکه زبان صفحه کلید انگلیسی (یا مشابه آن) باشد. برای اینکار کد زیر به عنوان راه حل بهتر پیشنهاد میشود:
<html>
<body>
  <input id="inputText" type="text" />
  <div id="divCapsLock" style="color: Red; display: none;">Caps Lock is ON!</div>
  <script>
    var capsLock = null;
    var hasFocus = false;
    document.onkeyup = keyUp;
    document.onkeypress = keyPress;
    document.getElementById('inputText').onfocus = focus;
    document.getElementById('inputText').onblur = focus;
    function warnCapsLock() {
      document.getElementById('divCapsLock').style.display = (capsLock != null && capsLock && hasFocus) ? 'block' : 'none';
    }
    function focus() {
      hasFocus = !hasFocus;
      warnCapsLock();
    }
    function keyUp(e) {
      e = e || event;
      capsLock = (e.keyCode == 20 && capsLock !== null) ? !capsLock : capsLock;
      warnCapsLock();
    }
    function keyPress(e) {
      if (capsLock != null) return;
      e = e || window.event;
      var charCode = e.charCode || e.keyCode;
      capsLock = (charCode >= 97 && charCode <= 122 && e.shiftKey) || (charCode >= 65 && charCode <= 90 && !e.shiftKey);
      warnCapsLock();
    }
  </script>
</body>
</html>
اگر دقت کنین میبیند که رویداد keydown در این کد نهایی با keyup جایگزین شده است. درصورت استفاده از رویداد keypress یک مشکل کوچک بوجود می‌آید و آن این است که اگر کاربر کلید caps lock را فشرده و سپس آن را در حالت فشرده نگه دارد اتفاق بدی خواهد افتاد! در این حال چون رویداد keydown مرتبا فراخوانده میشود وضعیت متغیر capsLock در این کد کاملا نامعتبر خواهد بود. بنابراین بهتر است تا از رویداد keyup که تنها یکبار فراخوانده میشود استفاده شود.
نکته: اگر کاربر پس از مشخص شدن وضعیت کلید Capa Lock در این کد، فوکس را به پنجره دیگری تغییر داده و سپس وضعیت این دکمه در آنجا تغییر دهد این کد دیگر درست کار نخواهد کرد. برای حل این مشکل هم راه حل زیر وجود دارد:
window.onblur = function () { capsLock = null; }
با این کار وضعیت متغیر capsLock ریست میشود.
نکته: در آزمایش این کد دقت کنید که زبان کیبرد حتما انگلیسی باشد.

مدیریت کلیدها
برای مدیریت کلیدهای کیبرد راههای متنوعی وجود دارد. مثلا میتوان از قطعه کد زیر استفاده کرد:
var Client = {};
Client.Keyboard = {};
Client.Keyboard.EnableKeyDown = true;
Client.Keyboard.EnableKeyUp = false;
Client.Keyboard.CurrentKeyEvent = null;

window.onkeydown = function (event) {
  if (!Client.Keyboard.EnableKeyDown) return true;
  return KeyboardEvents(event);
};

window.onkeyup = function (event) {
  if (!Client.Keyboard.EnableKeyUp) return true;
  return KeyboardEvents(event);
};

function Rise(event) {
  var e = Client.Keyboard.CurrentKeyEvent;
  if (event) {
    var data = { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey };
    if (!event(data)) return false;
    return event(data);
  }
  return true;
}

function KeyboardEvents(e) {
  e = e || window.event;
  Client.Keyboard.CurrentKeyEvent = e;
  switch (e.keyCode) {
    case 8:
      return Rise(Client.Keyboard.Backspace);
    case 9:
      return Rise(Client.Keyboard.Tab);
    case 13:
      return Rise(Client.Keyboard.Enter);
    case 16:
      return Rise(Client.Keyboard.Shift);
    case 17:
      return Rise(Client.Keyboard.Ctrl);
    case 18:
      return Rise(Client.Keyboard.Alt);
    case 19:
      return Rise(Client.Keyboard.Pause);
    case 20:
      return Rise(Client.Keyboard.CapsLock);
    case 27:
      return Rise(Client.Keyboard.Esc);
    case 33:
      return Rise(Client.Keyboard.PageUp);
    case 34:
      return Rise(Client.Keyboard.PageDown);
    case 35:
      return Rise(Client.Keyboard.End);
    case 36:
      return Rise(Client.Keyboard.Home);
    case 37:
      return Rise(Client.Keyboard.Left);
    case 38:
      return Rise(Client.Keyboard.Up);
    case 39:
      return Rise(Client.Keyboard.Right);
    case 40:
      return Rise(Client.Keyboard.Down);
    case 44:
      return Rise(Client.Keyboard.PrtScr);
    case 45:
      return Rise(Client.Keyboard.Insert);
    case 46:
      return Rise(Client.Keyboard.Delete);
      //////////////////////////////////////////////////////////////////////////////////////////////////
    case 48:
      return Rise(Client.Keyboard.Num0);
    case 49:
      return Rise(Client.Keyboard.Num1);
    case 50:
      return Rise(Client.Keyboard.Num2);
    case 51:
      return Rise(Client.Keyboard.Num3);
    case 52:
      return Rise(Client.Keyboard.Num4);
    case 53:
      return Rise(Client.Keyboard.Num5);
    case 54:
      return Rise(Client.Keyboard.Num6);
    case 55:
      return Rise(Client.Keyboard.Num7);
    case 56:
      return Rise(Client.Keyboard.Num8);
    case 57:
      return Rise(Client.Keyboard.Num9);
      //////////////////////////////////////////////////////////////////////////////////////////////////
    case 65:
      return Rise(Client.Keyboard.A);
    case 66:
      return Rise(Client.Keyboard.B);
    case 67:
      return Rise(Client.Keyboard.C);
    case 68:
      return Rise(Client.Keyboard.D);
    case 69:
      return Rise(Client.Keyboard.E);
    case 70:
      return Rise(Client.Keyboard.F);
    case 71:
      return Rise(Client.Keyboard.G);
    case 72:
      return Rise(Client.Keyboard.H);
    case 73:
      return Rise(Client.Keyboard.I);
    case 74:
      return Rise(Client.Keyboard.J);
    case 75:
      return Rise(Client.Keyboard.K);
    case 76:
      return Rise(Client.Keyboard.L);
    case 77:
      return Rise(Client.Keyboard.M);
    case 78:
      return Rise(Client.Keyboard.N);
    case 79:
      return Rise(Client.Keyboard.O);
    case 80:
      return Rise(Client.Keyboard.P);
    case 81:
      return Rise(Client.Keyboard.Q);
    case 82:
      return Rise(Client.Keyboard.R);
    case 83:
      return Rise(Client.Keyboard.S);
    case 84:
      return Rise(Client.Keyboard.T);
    case 85:
      return Rise(Client.Keyboard.U);
    case 86:
      return Rise(Client.Keyboard.V);
    case 87:
      return Rise(Client.Keyboard.W);
    case 88:
      return Rise(Client.Keyboard.X);
    case 89:
      return Rise(Client.Keyboard.Y);
    case 90:
      return Rise(Client.Keyboard.Z);
      ////////////////////////////////////////////////////////////////////////////
    case 91:
      //case 219: // opera
      return Rise(Client.Keyboard.LeftWindow);
    case 92:
      return Rise(Client.Keyboard.RightWindow);
    case 93:
      return Rise(Client.Keyboard.ContextMenu);
      ////////////////////////////////////////////////////////////////////////////
    case 96:
      return Rise(Client.Keyboard.NumPad0);
    case 97:
      return Rise(Client.Keyboard.NumPad1);
    case 98:
      return Rise(Client.Keyboard.NumPad2);
    case 99:
      return Rise(Client.Keyboard.NumPad3);
    case 100:
      return Rise(Client.Keyboard.NumPad4);
    case 101:
      return Rise(Client.Keyboard.NumPad5);
    case 102:
      return Rise(Client.Keyboard.NumPad6);
    case 103:
      return Rise(Client.Keyboard.NumPad7);
    case 104:
      return Rise(Client.Keyboard.NumPad8);
    case 105:
      return Rise(Client.Keyboard.NumPad9);
      ////////////////////////////////////////////////////////////////////////////
    case 106:
      return Rise(Client.Keyboard.Multiply);
    case 107:
      return Rise(Client.Keyboard.Add);
    case 109:
      return Rise(Client.Keyboard.Subtract);
    case 110:
      return Rise(Client.Keyboard.DecimalPoint);
    case 111:
      return Rise(Client.Keyboard.Divide);
      ////////////////////////////////////////////////////////////////////////////
    case 112:
      return Rise(Client.Keyboard.F1);
    case 113:
      return Rise(Client.Keyboard.F2);
    case 114:
      return Rise(Client.Keyboard.F3);
    case 115:
      return Rise(Client.Keyboard.F4);
    case 116:
      return Rise(Client.Keyboard.F5);
    case 117:
      return Rise(Client.Keyboard.F6);
    case 118:
      return Rise(Client.Keyboard.F7);
    case 119:
      return Rise(Client.Keyboard.F8);
    case 120:
      return Rise(Client.Keyboard.F9);
    case 121:
      return Rise(Client.Keyboard.F10);
    case 122:
      return Rise(Client.Keyboard.F11);
    case 123:
      return Rise(Client.Keyboard.F12);
      ////////////////////////////////////////////////////////////////////////////
    case 144:
      return Rise(Client.Keyboard.NumLock);
    case 145:
      return Rise(Client.Keyboard.ScrollLock);
    case 186:
    case 59: // opera & firefox
      return Rise(Client.Keyboard.SemiColon);
    case 187:
    case 61: // opera
      //case 107: //firefox
      return Rise(Client.Keyboard.Equal);
    case 188:
      return Rise(Client.Keyboard.Camma);
    case 189:
      return Rise(Client.Keyboard.Dash);
    case 190:
      return Rise(Client.Keyboard.Period);
    case 191:
      return Rise(Client.Keyboard.Slash);
    case 192:
      return Rise(Client.Keyboard.GraveAccent);
    case 219:
      return Rise(Client.Keyboard.OpenBracket);
    case 220:
      return Rise(Client.Keyboard.BackSlash);
    case 221:
      return Rise(Client.Keyboard.CloseBracket);
    case 222:
      return Rise(Client.Keyboard.SingleQuote);
  }
};
نحوه استفاده از این کد به صورت زیر است:
  // ctrl + s
  Client.Keyboard.S = function (e) {
    if (e.ctrl) {
      // انجام عملیات موردنظر
    }
    return true;
  }
اگر در متد الصاق شده به پراپرتیهای Client.Keyboard هیچ مقداری برگشت داده نشود، باتوجه به کد موجود در متد Rise، عملیات پیشفرض کلید مربوطه در پنجره مرورگر غیرفعال خواهد شد. بنابراین در کد بالا بعد از شرط، مقدار true برگشت داده شده است.
هرچند سعی کردم که تفاوت میان مرورگرهای مختلف را درنظر بگیرم ولی احتمال اینکه برخی از کدها به دلیل تفاوت میان مرورگرهای مختلف در کد بالا آورده نشده باشد وجود دارد!
نکته: کد فوق بیشتر برای روشن‌تر شدن موضوع ارائه شده است چون راه حل‌های بهتری هم برای مدیریت کلیدهای کیبرد وجود دارد. مثل استفاده از کتابخانه های Mousetrap یا HotKey یا jQuery Hotkeys یا KeyboardJS. البته در این میان بهترین کتابخانه موجود به نظر من همان Mousetrap است که تنها کتابخانه موجود است که علاوه بر پشتیبانی از ترکیب کلیدها با کلیدهای کنترلی، از توالی کلیدها (keys sequence) نیز پشتیبانی میکند که کار جالبی است.
در پایان این نکته را یادآور میشوم که امروزه توسعه اپلیکیشنهای تحت وب بدون استفاده مناسب از امکانات سمت کلاینت طرفداری ندارد. پس بهتر است هرچه بیشتر در مورد این زبان مرموز و پر از رمز و راز (JavaScript) بدانیم.
مطالب
سازگار کردن GridView با افزونه‌های jQuery


افزونه‌ها/پلاگین‌های زیادی جهت کار با table استاندارد HTML وجود دارند و خروجی رندر شده‌ی یک ASP.Net GridView هم در نهایت یک جدول است. فرض کنید قصد داریم افزونه زیر را به GridView استاندارد ASP.Net اعمال کنیم.
jQuery quickSearch plug-in

ظاهرا بدون مشکل خاصی اعمال می‌گردد. برای مثال در هدر صفحه داریم: (شبیه به مثال موجود در سایت اصلی آن، جهت اعمال به GridView1)

<script src="jquery.min.js" type="text/javascript"></script>
<script src="jquery.quicksearch.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
//جستجو در جدول
$('table#<%=GridView1.ClientID %> tbody tr').quicksearch({
position: 'before',
attached: 'span#attachSearch',
labelText: 'جستجو',
isFieldset: true,
loaderText: ' ... ',
fixWidths: true
});
});
</script>

برای اعمال بر:
(در اینجا محل قرارگیری تکست باکس مربوط به جستجو، در span ایی با id مساوی attachSearch تنظیم شده است، می‌توانید از ID خود GridView هم استفاده کنید.)

<span id="attachSearch"></span>
<br />
<asp:GridView ID="GridView1" runat="server"></asp:GridView>

نکته:
1- همانطور که در مقاله مربوط به ClientID ذکر شد، هیچ الزامی ندارد که ID‌ مربوط به GridView‌ شما برای مثال دقیقا همان GridView1 جهت استفاده در سمت کلاینت باشد و بسته به container آن، این نام ترکیبی از ID شیء(های) در بر گیرنده و شیء مورد نظر می‌باشد. به همین جهت از GridView1.ClientID استفاده گردید تا اسکریپت ما با آن مشکلی نداشته باشد.

2- خصوصیات ظاهری افزونه فوق از سلکتور quicksearch فایل css شما دریافت می‌شوند. برای مثال:
.quicksearch
{
width:190px;
}

مشکل!
پس از هر بار جستجو، header مربوط به GridView محو می‌شود، اما نمونه موجود در سایت اصلی افزونه، این مشکل را ندارد. چرا؟!
GridView‌ مربوط به ASP.Net پس از رندر شدن، جدولی است که تگ‌های thead را ندارد. اگر به سورس صفحه سایت افزونه دقت نمائید، هدر جدول با تگ‌های thead محصور شده است اما GridView استاندارد ASP.Net به صورت پیش فرض این‌کار را انجام نمی‌دهد و خروجی آن چیزی شبیه به جدول زیر است: (هدر با th مشخص می‌شود و از تگ thead خبری نیست)

<table >
<tr >
<th scope="col">col1</th>
<th scope="col">col2</th>
<th scope="col">col3</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
.
.
.

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

//سازگار با افزونه‌های جی کوئری
if (GridView1.Rows.Count > 0)
{
//This replaces <td> with <th> and adds the scope attribute
GridView1.UseAccessibleHeader = true;

//This will add the <thead> and <tbody> elements
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader;

//This adds the <tfoot> element.
//Remove if you don't have a footer row
//GridView1.FooterRow.TableSection = TableRowSection.TableFooter;
}

ب)راه حل سمت کلاینت
سطر مربوط به جستجو را به صورت زیر هم می‌توان نوشت:

$('table#<%=GridView1.ClientID %> tr:has(td)').quicksearch({

در اینجا به دنبال هرچی tr که td دارد می‌گردیم. به صورت پیش فرض در tr مربوط به هدر گرید، ما th داریم و نه td ، بنابراین مشکل محو شدن هدر به این صورت حل خواهد شد.

نکته:
اگر می‌خواهید که ناحیه مربوط به جستجوی افزونه فوق در پرینت صفحه ظاهر نشود به css صفحه چند سطر زیر را اضافه کنید:

@media print
{
.quicksearch
{
display: none;
}
}


تمرین!
افزونه جی کوئری زیر را به یک ASP.Net GridView اعمال کنید:
table sorter

مطالب
معرفی کتابخانه Loader برای بارگذاری JS و CSS
در طراحی صفحات وب، معمولا از فایل‌های JS و CSS مختلفی استفاده می‌شود؛ از کتابخانه‌ها گرفته تا فایل‌ها اصلی برنامه. به صورت خیلی ساده ما تمام این فایل‌ها را به صفحه‌ی لینک می‌کنیم. اما این روش درست نیست و حجم صفحه و تعداد درخواست‌ها به سرور برای بارگذاری فایل‌ها خیلی بیشتر می‌شود. در زمان اجرای یک صفحه‌ی وب مسلما قسمت‌هایی از صفحه وجود دارند که شاید در شرایط خاصی، کاربر این صفحات را ببیند و یا نیاز باشد تا منطقی، توسط یک فایل JS خاص انجام شود. کتابخانه‌های زیادی برای حل این موضوع درست شده‌اند که راهکار آنها به این صورت است که شما در مواقعی که نیاز به این فایل‌ها دارید، آنها را بارگذاری می‌کنید. کتابخانه‌ی Loader یک فایل JS ساده می‌باشد که توسط اینجانب نوشته شده است و در یک پروژه‌ی بزرگ در حال استفاده است. این کتابخانه تا همین الان که 4 سال از عمر پروژه می‌گذرد در حال کار کردن هست و بدون هیچ مشکلی تا الان جواب داده است. بنابراین تصمیم گرفتم تا این کتابخانه را به صورت عمومی منتشر کنم تا شما هم از این کتابخانه استفاده کنید. در زیر کد Core این کتابخانه و نحوه‌ی استفاده از آن را نوشته‌ام و لینک GitHub هم در زیر می‌باشد.

نحوه‌ی استفاده از این کتابخانه بعد از اینکه فایل JS آن‌را به صفحه وصل کردیم، به صورت زیر است که می‌توانید بر حسب نیاز، این تابع را صدا بزنید. کد زیر نحوه‌ی استفاده از این کتابخانه هست. فرض کنید در شرایطی نیاز داریم تا کتابخانه‌ی JSTree را بارگذری کنیم. به جای اینکه از اول فایل‌های JS و CSS آن‌را در صفحه داشته باشیم، خیلی ساده از تابع زیر استفاده می‌کنیم. در این کتابخانه تابع Promise وقتی Fire می‌شود که تمام فایل‌هایی که به صورت پارامتر در تابع Load مشخص شده‌اند، بارگذاری شوند.
loader.load([
  'plugin/dropdowntree/css/style.min.css',  
  'plugin/dropdowntree/js/jstree.js',  
  'plugin/dropdowntree/js/jstree.checkbox.js',  
]).promise(function () {
  // run this code promise  
});
کد هسته‌ی اصلی کتابخانه Loader به صورت زیر هست:
/*     
    loader version 0.2.1 2015    
    loader design by Behnam Mohammadi (http://itten.ir)
*/
window.loader = {  
    load: function (urls) {
        var loadCounter = 0;
        var promise = null;
        var ext = '';
        this.promise = function (fun) {
            promise = fun;
        };
        for (var i = 0; i < urls.length; i++) {
            ext = urls[i].substring(urls[i].length - 3);
            if (ext == '.js') {
                var script = document.createElement('script');
                script.src = urls[i];
                script.onload = function () {
                    loadCounter += 1;
                    if (loadCounter == urls.length) {
                        promise();
                    }
                };
                document.body.appendChild(script);
            } else if (ext == 'css') {
                var link = document.createElement('link');
                link.href = urls[i];
                link.rel = 'stylesheet';
                link.type = 'text\css';
                link.onload = function () {
                    loadCounter += 1;
                    if (loadCounter == urls.length) {
                        promise();
                    }
                };
                document.body.appendChild(link);
            }
        }
        return this;
    }
}
حجم این کتابخانه در صورت فشرده سازی کمتر از نیم کیلوبایت هست. لینک GitHub این پروژه را در زیر مشاهده میکنید.
مطالب دوره‌ها
ارائه کاربری ساده‌تر انتخاب چندین آیتم از یک لیست به کمک افزونه TagIt در ASP.NET MVC
چندی قبل مطلبی را در مورد بررسی تفصیلی رابطه چند به چند در این سایت مطالعه کردید. در آن مطلب صرفا به بحث ذخیره سازی اطلاعات دریافتی از کاربر اشاره شد. برای مثال اگر مطلبی چندین برچسب دارد، چگونه باید این‌ها را در بانک اطلاعاتی به نحو صحیحی ذخیره کرد.
در مطلب جاری قصد داریم با نحوه ارائه یک UI کاربر پسند برای این منظور آشنا شویم و سؤال مهم هم این است: «چگونه می‌توان کار کاربر را در حین وارد کردن تعدادی از برچسب‌های مرتبط با یک مطلب ساده‌تر کرد؟». برای این منظور یکی از راه حل‌هایی که در بسیاری از سایت‌ها مرسوم شده است، استفاده از افزونه‌هایی مانند jQuery TagIt می‌باشد که در ادامه با نحوه استفاده از آن در ASP.NET MVC آشنا خواهیم شد.


پیشنیازها:
دریافت افزونه TagIt
همچنین دریافت jQuery UI (افزونه TagIt برای نمایش لیست Auto Complete آیتم‌ها از jQuery UI در پشت صحنه استفاده می‌کند)
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/TagIt/tagit-simple-blue.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Content/TagIt/jquery-ui-1.8.23.custom.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Content/TagIt/tagit.js")" type="text/javascript"></script>
    @RenderSection("JavaScript", required: false)
</head>
که نهایتا نیاز است یک چنین تعاریفی را به فایل layout برنامه اضافه کنیم.

آشنایی با مدل برنامه

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace jQueryMvcSample04.Models
{
    public class BlogPostViewModel
    {
        [DisplayName("عنوان"), Required(ErrorMessage = "*")]
        public string Title { set; get; }

        [DisplayName("متن"), Required(ErrorMessage = "*")]
        public string Body { set; get; }

        /// <summary>
        /// آرایه‌ای محدود از برچسب‌های این مطلب خاص به صورت جی‌سون که پیشتر ثبت شده است
        /// هدف استفاده در حین ویرایش مطلب
        /// </summary>
        public string InitialTags { set; get; }

        /// <summary>
        /// آرایه‌ای جی‌سونی از تمام برچسب‌های موجود در سیستم
        /// هدف نمایش منوی انتخاب برچسب‌ها از لیست
        /// </summary>
        public string TagsSource { set; get; }

        /// <summary>
        /// آرایه‌ای از برچسب‌های وارد شده توسط کاربر در حین ثبت مطلب
        /// </summary>
        [DisplayName("برچسب‌ها"), Required(ErrorMessage = "*")]
        public string[] Tags { set; get; }

        public int? Id { set; get; }
    }
}
اگر به نام این کلاس دقت کنید، به ViewModel ختم شده است. از این لحاظ که حاوی خواصی می‌باشد که عموما جهت رندر کردن صحیح UI مورد استفاده قرار می‌گیرند و معادلی در سمت بانک اطلاعاتی نخواهند داشت.
افزونه TagIt برای نمایش اطلاعات خود به دو منبع داده نیاز دارد:
الف) TagsSource : لیستی است به فرمت JSON، از هر آنچه که در سیستم پیشتر به عنوان یک برچسب ثبت شده است. از این لیست برای نمایش منوی خودکار انتخاب آیتم‌ها استفاده می‌شود.
ب) InitialTags : لیستی است به فرمت JSON، از تمام برچسب‌های مرتبط با یک مطلب. از این اطلاعات در حین ویرایش یک مطلب استفاده خواهد شد.

در این ViewModel یک خاصیت دیگر به شکل آرایه، به نام Tags تعریف شده است که لیست برچسب‌های وارد شده توسط کاربر را دریافت خواهد کرد.


معرفی کنترلر برنامه

using System.Web.Mvc;
using jQueryMvcSample04.Extensions;
using jQueryMvcSample04.Models;

namespace jQueryMvcSample04.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index(int? id)
        {
            //در ابتدای کار تمام تگ‌های موجود در سیستم از بانک اطلاعاتی دریافت خواهند شد
            //از این تگ‌ها برای تشکیل منوی انتخاب برچسب‌ها استفاده می‌شود
            var tagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson();

            //همچنین صرفا برچسب‌های مطلب جاری که پیشتر ثبت شده‌اند نیز باید از بانک اطلاعاتی دریافت گردند
            //از این برچسب‌ها برای ویرایش یک مطلب موجود استفاده خواهد شد
            var init = new[] { "ASP.NET" }.ToJson();

            var model = new BlogPostViewModel
            {
                TagsSource = tagsSource,
                InitialTags = init,
                Id = id
            };
            return View(model);
        }

        [HttpPost]
        public ActionResult Index(BlogPostViewModel data)
        {
            if (this.ModelState.IsValid)
            {
                //todo: save data
                // ...
                return RedirectToAction(actionName: "index", controllerName: "home");
            }

            //در صورت بروز خطا مجددا اطلاعات موجود نمایش داده خواهند شد
            data.TagsSource = new[] { "C#", "C++", "C", "ASP.NET", "MVC" }.ToJson();
            data.InitialTags = data.Tags.ToJson();
            return View(data);
        }
    }
}


با توجه به توضیحاتی که ارائه شد، کنترلر برنامه ساختار واضح‌تری را یافته است. در اولین بار نمایش صفحه، لیست منبع داده تگ‌ها و همچنین تگ‌های مرتبط با یک مطلب (در صورت وجود) به View ارائه خواهند شد.
از همین ViewModel، در عملیات Post نیز استفاده گردیده و اطلاعات دریافت می‌گردد.
تعریف متد الحاقی ToJson مورد استفاده را نیز در ادامه ملاحظه می‌نمائید:
using System.Linq;
using System.Web.Script.Serialization;

namespace jQueryMvcSample04.Extensions
{
    public static class JsonExt
    {
        public static string ToJson(this string[] initialTags)
        {            
            if (initialTags == null || !initialTags.Any())
                return "[]";
            else
                return new JavaScriptSerializer().Serialize(initialTags);
        }
    }
}

و مرحله آخر تعریف View متناظر است

@model jQueryMvcSample04.Models.BlogPostViewModel
@{
    ViewBag.Title = "Index";
}
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>ثبت مطلب جدید</legend>
        @Html.HiddenFor(model => model.Id)
        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Body)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Body)
            @Html.ValidationMessageFor(model => model.Body)
        </div>
        <div class="editor-label">
            @Html.LabelFor(model => model.Tags)
        </div>
        <div class="editor-field">
            <ul id="tagsList" dir="ltr" name="Tags">
            </ul>
            @Html.ValidationMessageFor(model => model.Tags)
        </div>
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}
@section JavaScript
{
    <script type="text/javascript">
    $(document).ready(function () {
            var tagsSource = @Html.Raw(Model.TagsSource);
            $('#tagsList').tagit({
                 tagSource: tagsSource, 
                 select: true, 
                 triggerKeys: ['enter', 'comma', 'tab'],
                 initialTags:  @Html.Raw(Model.InitialTags) 
              });
});
    </script>
}
در این View دو نکته حائز اهمیت هستند:
الف) برای نمایش افزونه TagIt از یک ul با id مساوی tagsList استفاده شده است.
ب) خواص اضافی موجود در ViewModel که اطلاعات JSON ایی مورد نیاز را بازگشت می‌دهند در قسمت اسکریپت صفحه مورد استفاده قرار گرفته‌اند. در اینجا نیاز است از Html.Raw استفاده شود تا اطلاعات مرتبط با JSON اشتباها encode نشده و قابل استفاده باشند.

دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample04.zip
مطالب
خلاصه‌ای در مورد روش‌های دریافت فایل از سایت NuGet
بهبود سرعت دریافت بسته‌های نیوگت

در کشور بسیاری از اوقات دسترسی به پروتکل HTTPS به کندی صورت می‌گیرد. گاهی از اوقات نیز این دسترسی غیر ممکن می‌شود تا حد دریافت چند بایت در دقیقه. همین مساله تاکنون بر روی بسیاری از مسایل دیگر نیز تاثیر گذار بوده است؛ برای مثال اگر یک مخزن کد را مثلا در CodePlex یا GitHub داشته باشید، چون تمام Commitها از طریق همین پروتکل امن صورت می‌گیرد، کار کردن با آن‌ها بسیار مشکل خواهد شد. نمونه‌ی دیگر آن دسترسی به NuGet است. فید NuGet در VS.NET به Https تنظیم شده است. اگر دسترسی به Https برای شما به کندی صورت می‌گیرد فقط کافی است مسیر فید آن‌را در منوی Tools، گزینه‌ی Options، ذیل قسمت Package manager یافته و به http://nuget.org/api/v2 تغییر دهید؛ یعنی به Http خالی، بجای Https؛ تا سرعت دریافت بسته‌های NuGet مورد نظر افزایش یابند.


دریافت مستقیم بسته‌های نیوگت

برای دریافت بسته‌های نیوگت که دارای پسوند nupkg هستند، اما در اصل یک فایل zip بیشتر نیستند، الزامی به استفاده از ابزار و افزونه نیوگت در VS.NET نیست. می‌توان این بسته‌ها را به صورت مستقیم نیز دریافت کرد. برای مثال اگر آدرس بسته‌ای در سایت NuGet به صورت زیر است:
https://www.nuget.org/packages/PropertyChanged.Fody
برای دریافت مستقیم آن کافی است آدرس ذیل را درخواست کنید:
https://www.nuget.org/api/v2/package/PropertyChanged.Fody/1.42.0
یک api/v2 به این لینک اضافه می‌شود به همراه شماره نگارش مدنظر برای دریافت:
 https://www.nuget.org/api/v2/package/{packageID}/{packageVersion}
و یا برای مثال در سایت نیوگت عضو شوید و سپس به آن لاگین کنید. به این ترتیب با مراجعه به هر کتابخانه‌ای که در آنجا آپلود شده، یک لینک download در کنار صفحه، سمت چپ ظاهر می‌شود. با کلیک بر روی آن فایل nupkg آن کتابخانه قابل دریافت خواهد بود. این فایل در حقیقت یک فایل zip است. بنابراین کار کردن با محتویات آن ساده‌است.
به صورت خلاصه:
لینک اصلی کتابخانه: https://www.nuget.org/packages/Twitter.Bootstrap.RTL.Less/3.0.0
لینک دانلود آن: https://www.nuget.org/api/v2/package/Twitter.Bootstrap.RTL.Less/3.0.0

راه دیگر، ساخت دستی این آدرس است:
https://az320820.vo.msecnd.net/packages/propertychanged.fody.1.42.0.nupkg
که در حقیقت تشکیل شده است از:
 https://az320820.vo.msecnd.net/packages/{name}.{version}.nupkg
اگر نام آخرین بسته ارسالی PropertyChanged.Fody 1.42.0 باشد. فقط کافی است این دو قسمت را، یعنی نام و شماره نگارش را با یک نقطه به هم متصل کنید و سپس به انتهای آن، پسوند nupkg را اضافه نمائید. این فایل در آدرس https://az320820.vo.msecnd.net/packages/ به صورت مستقیم قابل دریافت است.


اهمیت تنظیمات IE

اگر پیام قطع شدن اتصال یا مشکلات DNS را در کنسول NuGet در VS.NET دریافت می‌کنید، ابتدا سعی کنید همان روش ذکر شده در ابتدای بحث را امتحان کنید. اگر کار نکرد احتمالا مشکل از تنظیمات IE است. برای مثال اگر بر روی تنظیمات اتصالی شما در IE یک پروکسی غیرقابل دسترسی در زمان جاری، تنظیم شده باشد، این مساله مستقیما بر روی اتصالات برنامه‌های دات نتی نیز تاثیر گذار است. بنابراین ابتدا لینک nupkg را که ساخته‌اید یکبار با IE امتحان کنید. اگر قابل دریافت نبود یعنی تنظیمات آن به هم ریخته است و این مساله بر روی بسیاری از برنامه‌های دیگر نیز تاثیر گذار است.
مطالب
مدیریت محل اعمال Google analytics در ASP.NET MVC
ذخیره سازی اطلاعات بازدیدهای کاربران، در طول زمان حجم بالایی از بانک اطلاعاتی را به خود اختصاص خواهد داد؛ به علاوه کند شدن کوئری‌های مرتبط با آن، به همراه مصرف بالای منابع سیستم. به همین جهت اکثر سایت‌ها از Google analytics برای مدیریت جمع آوری بازدیدهای کاربران خود استفاده می‌کنند و این ابزار واقعا عالی و حرفه‌ای طراحی شده و پیاده سازی همانند آن شاید در حد یک پروژه‌ی‌ چندساله باشد.
اضافه کردن Google analytics به یک سایت، بسیار ساده است. در آن ثبت نام می‌کنید؛ سپس آدرس دومین خود را وارد کرده و یک قطعه کد جاوا اسکریپتی را دریافت خواهید کرد که باید به انتهای تمام صفحات سایت خود اضافه نمائید و ... همین.
اضافه کردن این کد در ASP.NET MVC می‌تواند در فایل layout یا همان master page سایت انجام شود تا به صورت خودکار به تمام صفحات اعمال گردد.

مشکل!
من نمی‌خواهم که صفحات غیرعمومی سایت نیز دارای کدهای Google analytics باشند و بی‌جهت Google به این‌جاها نیز سرکشی زاید کند! چکار باید کرد؟
احتمالا عنوان می‌کنید که باید یک if و else به همراه آرایه‌ای از نام‌ها و آدرس‌های صفحات غیرعمومی سایت تهیه کرد و بر این اساس کدهای Google analytics را در master page درج کرد یا خیر.
بله. این روش کار می‌کنه ولی بهینه نیست و همچنین نگهداری آن در طول زمان مشکل است. سایت توسعه خواهد یافت، صفحات غیرعمومی بیشتر خواهند شد و ممکن است در این بین فراموش شود که کدهای مرتبط به روز شوند.

روش بهتر:
آیا می‌توان در یک View مشخص کرد که فیلتر Authorize در اکشن متد متناظری که آن‌را رندر کرده است بکار گرفته شده است یا خیر؟
صفحات غیرعمومی سایت در ASP.NET MVC با فیلتر Authorize محافظت می‌شوند. این فیلتر را می‌توان به کل یک کنترلر اعمال کرد تا به تمام اکشن متدهای آن اعمال شود؛ یا فقط به یک اکشن متد خاص که Viewایی خاص را رندر می‌کند.

نحوه پیاده سازی تشخیص وجود فیلتر Authorize را در یک View رندر شده، در متد کمکی زیر می‌توان مشاهده کرد:
@helper IncludeGoogleAnalytics(WebViewPage page)
    {
        var controller = page.ViewContext.Controller;
        var controllerHasAuthorizeAttribute = controller.GetType().GetCustomAttributes(typeof(AuthorizeAttribute), true).Any();
        var currentActionName = page.ViewContext.Controller.ValueProvider.GetValue("action").RawValue.ToString();
        var actionHasAuthorizeAttribute = controller.GetType().GetMethods()
                                                    .Where(x => x.Name == currentActionName &&
                                                                x.GetCustomAttributes(typeof(AuthorizeAttribute), true).Any())                                                    
                                                    .Any();
        if (!controllerHasAuthorizeAttribute && !actionHasAuthorizeAttribute)
        {
            string trackingId = ConfigurationManager.AppSettings["GoogleAnalyticsID"];
    <script type="text/javascript">
        var _gaq = _gaq || [];
        _gaq.push(['_setAccount', '@trackingId']);
        _gaq.push(['_trackPageview']);
        (function () {
            var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
            ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
            var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
        })();    
    </script>
        }

  توضیحات:
این متد در فایلی به نام HtmlUtils قرار گرفته در پوشه app_code تعریف شده است و بکارگیری آن در یک فایل master page به نحو زیر خواهد بود:
@HtmlUtils.IncludeGoogleAnalytics(this)

در این متد به کمک خاصیت page.ViewContext.Controller می‌توان به کنترلری که در حال رندر کردن View جاری است دسترسی یافت. اکنون که به کنترلر دسترسی داریم، به کمک Reflection، ویژگی‌ها یا Attributes آن‌را یافته و بررسی می‌کنیم که آیا دارای AuthorizeAttribute است یا خیر. بر این اساس می‌توان تصمیم گرفت که آیا View در حال نمایش عمومی است یا خصوصی. اگر عمومی بود، کدهای اسکریپتی Google analytics به صورت خودکار به صفحه تزریق می‌شوند.
همچنین در اینجا فرض بر این است که Id منتسب به دومین جاری در کلیدی به نام GoogleAnalyticsID در فایل کانفیگ برنامه در قسمت app settings آن تعریف شده است.
 
مطالب
حل مشکل بارگذاری اولیه دستورات جاوا اسکریپتی در پروژه‌های Blazor
مشکل:
ممکن است بخواهید در برنامه‌های Blazor از یک قطعه کد آماده استفاده نمایید که در آن از دستورات Javascript استفاده شده باشد و تعدادی رویداد برای المان‌های صفحه تعریف کرده باشند؛ به عنوان مثال من از قالب آماده Nice Admin استفاده می‌کنم که در آن برای تمام قالب، از یک فایل به نام main.js استفاده شده‌است و در آن برای مخفی و ظاهر نمودن منو، از یک دکمه toggle استفاده کرده‌است. برای این عملیات، یک رویداد کلیک در این فایل تعریف شده‌:
/**
* Template Name: NiceAdmin - v2.1.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
*/
(function() {
  "use strict";

  /**
   * Easy selector helper function
   */
  const select = (el, all = false) => {
    el = el.trim()
    if (all) {
      return [...document.querySelectorAll(el)]
    } else {
      return document.querySelector(el)
    }
  }

  /**
   * Easy event listener function
   */
  const on = (type, el, listener, all = false) => {
    if (all) {
      select(el, all).forEach(e => e.addEventListener(type, listener))
    } else {
      select(el, all).addEventListener(type, listener)
    }
  }

  /**
   * Easy on scroll event listener 
   */
  const onscroll = (el, listener) => {
    el.addEventListener('scroll', listener)
  }

  /**
   * Sidebar toggle
   */
  if (select('.toggle-sidebar-btn')) {
    on('click', '.toggle-sidebar-btn', function(e) {
      select('body').classList.toggle('toggle-sidebar')
    })
  }

  /**
   * Search bar toggle
   */
  if (select('.search-bar-toggle')) {
    on('click', '.search-bar-toggle', function(e) {
      select('.search-bar').classList.toggle('search-bar-show')
    })
  }
.
.
.
})();
و فراخوانی این فایل را در انتهای قسمت body فایل index.html یا Host.cshtml_ بصورت زیر قرار می‌دهیم:
<!DOCTYPE html>
<html dir="rtl">
.
.
.

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="">Reload</a>
        <a>🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>


    <!-- Vendor JS Files -->
    <script src="assets/vendor/bootstrap/js/bootstrap.bundle.js"></script>
    <script src="assets/vendor/php-email-form/validate.js"></script>
    <script src="assets/vendor/quill/quill.min.js"></script>
    <script src="assets/vendor/tinymce/tinymce.min.js"></script>
    <script src="assets/vendor/simple-datatables/simple-datatables.js"></script>
    <script src="assets/vendor/chart.js/chart.min.js"></script>
    <script src="assets/vendor/apexcharts/apexcharts.min.js"></script>
    <script src="assets/vendor/echarts/echarts.min.js"></script>
    <!-- Template Main JS File -->
    <script src="assets/js/main.js"></script>
</body>

</html>
و حالا که پروژه را اجرا کنید، رویداد کلیک بر روی دکمه‌ی toggle کار نمی‌کند!
دلیل:
مشکل به این دلیل می‌باشد که کدهای جاوا اسکریپتی، بلافاصله با دانلود فایل اجرا می‌شوند؛ در حالیکه بارگذاری صفحه هنوز توسط blazor به اتمام نرسیده‌است. در نتیجه المان‌هایی که در این فایل به آن‌ها اشاره شده‌است، هنوز قابل دسترسی نیستند و رویدادهای تعریف شده‌ی برای آن‌ها، اجرا نمی‌شوند.
راه حل:
باید اجرای کدهای جاوا اسکریپتی را تا بارگذاری کامل صفحه به تعویق بیاندازیم. برای همین منظور ابتدا کدهای تعریف شده‌ی در فایل main.js را بجای اینکه مستقیما اجرا شوند، در یک تابع قرار می‌دهیم:
/**
* Template Name: NiceAdmin - v2.1.0
* Template URL: https://bootstrapmade.com/nice-admin-bootstrap-admin-html-template/
* Author: BootstrapMade.com
* License: https://bootstrapmade.com/license/
*/
function initilizeNiceAdminJs() {
  "use strict";

  /**
   * Easy selector helper function
   */
  const select = (el, all = false) => {
    el = el.trim()
    if (all) {
      return [...document.querySelectorAll(el)]
    } else {
      return document.querySelector(el)
    }
  }

  /**
   * Easy event listener function
   */
  const on = (type, el, listener, all = false) => {
    if (all) {
      select(el, all).forEach(e => e.addEventListener(type, listener))
    } else {
      select(el, all).addEventListener(type, listener)
    }
  }

  /**
   * Easy on scroll event listener 
   */
  const onscroll = (el, listener) => {
    el.addEventListener('scroll', listener)
  }

  /**
   * Sidebar toggle
   */
  if (select('.toggle-sidebar-btn')) {
    on('click', '.toggle-sidebar-btn', function(e) {
      select('body').classList.toggle('toggle-sidebar')
    })
  }

  /**
   * Search bar toggle
   */
  if (select('.search-bar-toggle')) {
    on('click', '.search-bar-toggle', function(e) {
      select('.search-bar').classList.toggle('search-bar-show')
    })
  }

.
.
.
}
در مثال بالا من دستورات را داخل یک تابع به نام initilizeNiceAdminJs قرار دادم. سپس در فایل index.razor این تابع را در رویداد OnAfterRenderAsync  فراخوانی می‌نماییم:
  @page "/"

 @inject IJSRuntime JSRuntime

    <div>
        this is index page
    </div>
@code {

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JSRuntime.InvokeVoidAsync("initilizeNiceAdminJs");// Initialize main.js after site completely loaded
        }
    }
}