مطالب
ایجاد helper برای Nivo Slider در Asp.net Mvc

کامپوننت‌های jQuery زیادی وجود دارند که توسط آنها میتوان تصاویر را بصورت زمانبندی شده و به همراه افکت‌های زیبا در سایت خود نشان داد. مانند اینجا 

در این قصد ایجاد helper برای کامپوننت NivoSlider را داریم.

1- یک پروژه  Asp.net Mvc 4.0 ایجاد میکنیم.

2- سپس فایل jquery.nivo.slider.pack.js  ، فایل‌های css  مربوط به این کامپوننت و چهار تم موجود را از سایت این کامپوننت و یا درون سورس مثال ارائه شده دریافت می‌کنیم.

3- به کلاس BundleConfig رفته و کدهای زیر را اضافه میکنیم: 

#region Nivo Slider

   bundles.Add(new StyleBundle("~/Content/NivoSlider").Include("~/Content/nivoSlider/nivo-slider.css"));
   bundles.Add(new StyleBundle("~/Content/NivoSliderDefaultTheme").Include("~/Content/nivoSlider/themes/default/default.css"));
   bundles.Add(new StyleBundle("~/Content/NivoSliderDarkTheme").Include("~/Content/nivoSlider/themes/dark/dark.css"));
   bundles.Add(new StyleBundle("~/Content/NivoSliderLightTheme").Include("~/Content/nivoSlider/themes/light/light.css"));
   bundles.Add(new StyleBundle("~/Content/NivoSliderBarTheme").Include("~/Content/nivoSlider/themes/bar/bar.css"));
   bundles.Add(new ScriptBundle("~/bundles/NivoSlider").Include("~/Scripts/jquery.nivo.slider.pack.js"));

#endregion

سپس در فایل shared آنها را بصورت زیر اعمال میکنیم:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <script type="text/javascript" src="~/Scripts/jquery-1.9.1.min.js"></script>
    @Styles.Render("~/Content/NivoSlider", "~/Content/NivoSliderDefaultTheme","~/Content/NivoSliderLightTheme")
    @Scripts.Render("~/bundles/NivoSlider")
</head>
<body>
    @RenderBody()
    @RenderSection("scripts", required: false)
</body>
</html>

4- یک پوشه با عنوان helper به پروژه اضافه میکنیم. سپس کلاس‌های زیر را به آن اضافه می‌کنیم :

 public class NivoSliderHelper 
 {
        #region Fields

        private string _id = "nivo1";
        private List<NivoSliderItem> _models = null;
        private string _width = "100%";
        private NivoSliderTheme _theme = NivoSliderTheme.Default;
        private string _effect = "random"; // Specify sets like= 'fold;fade;sliceDown'
        private int _slices = 15; // For slice animations
        private int _boxCols = 8; // For box animations
        private int _boxRows = 4; // For box animations
        private int _animSpeed = 500; // Slide transition speed
        private int _pauseTime = 3000; // How long each slide will show
        private int _startSlide = 0; // Set starting Slide (0 index)
        private bool _directionNav = true; // Next & Prev navigation
        private bool _controlNav = true; // 1;2;3... navigation
        private bool _controlNavThumbs = false; // Use thumbnails for Control Nav
        private bool _pauseOnHover = true; // Stop animation while hovering
        private bool _manualAdvance = false; // Force manual transitions
        private string _prevText = "Prev"; // Prev directionNav text
        private string _nextText = "Next"; // Next directionNav text
        private bool _randomStart = false; // Start on a random slide
        private string _beforeChange = ""; // Triggers before a slide transition
        private string _afterChange = ""; // Triggers after a slide transition
        private string _slideshowEnd = ""; // Triggers after all slides have been shown
        private string _lastSlide = ""; // Triggers when last slide is shown
        private string _afterLoad = "";

        #endregion

        string makeParameters()
        {
            var builder = new StringBuilder();
            builder.Append("{");
            builder.Append(string.Format("effect:'{0}'", _effect));
            builder.AppendLine(string.Format(",slices:{0}", _slices));
            builder.AppendLine(string.Format(",boxCols:{0}", _boxCols));
            builder.AppendLine(string.Format(",boxRows:{0}", _boxRows));
            builder.AppendLine(string.Format(",animSpeed:{0}", _animSpeed));
            builder.AppendLine(string.Format(",pauseTime:{0}", _pauseTime));
            builder.AppendLine(string.Format(",startSlide:{0}", _startSlide));
            builder.AppendLine(string.Format(",directionNav:{0}", _directionNav.ToString().ToLower()));
            builder.AppendLine(string.Format(",controlNav:{0}", _controlNav.ToString().ToLower()));
            builder.AppendLine(string.Format(",controlNavThumbs:{0}", _controlNavThumbs.ToString().ToLower()));
            builder.AppendLine(string.Format(",pauseOnHover:{0}", _pauseOnHover.ToString().ToLower()));
            builder.AppendLine(string.Format(",manualAdvance:{0}", _manualAdvance.ToString().ToLower()));
            builder.AppendLine(string.Format(",prevText:'{0}'", _prevText));
            builder.AppendLine(string.Format(",nextText:'{0}'", _nextText));
            builder.AppendLine(string.Format(",randomStart:{0}", _randomStart.ToString().ToLower()));
            builder.AppendLine(string.Format(",beforeChange:{0}", _beforeChange));
            builder.AppendLine(string.Format(",afterChange:{0}", _afterChange));
            builder.AppendLine(string.Format(",slideshowEnd:{0}", _slideshowEnd));
            builder.AppendLine(string.Format(",lastSlide:{0}", _lastSlide));
            builder.AppendLine(string.Format(",afterLoad:{0}", _afterLoad));
            builder.Append("}");
            return builder.ToString();
        }
        public NivoSliderHelper (
            string id, 
            List<NivoSliderItem> models,
            string width = "100%", 
            NivoSliderTheme theme = NivoSliderTheme.Default,
            string effect = "random", 
            int slices = 15, 
            int boxCols = 8, 
            int boxRows = 4,
            int animSpeed = 500, 
            int pauseTime = 3000, 
            int startSlide = 0,
            bool directionNav = true, 
            bool controlNav = true,
            bool controlNavThumbs = false, 
            bool pauseOnHover = true, 
            bool manualAdvance = false,
            string prevText = "Prev",
            string nextText = "Next", 
            bool randomStart = false,
            string beforeChange = "function(){}", 
            string afterChange = "function(){}",
            string slideshowEnd = "function(){}", 
            string lastSlide = "function(){}", 
            string afterLoad = "function(){}")
        {
            _id = id;
            _models = models;
            _width = width;
            _theme = theme;
            _effect = effect;
            _slices = slices;
            _boxCols = boxCols;
            _boxRows = boxRows;
            _animSpeed = animSpeed;
            _pauseTime = pauseTime;
            _startSlide = startSlide;
            _directionNav = directionNav;
            _controlNav = controlNav;
            _controlNavThumbs = controlNavThumbs;
            _pauseOnHover = pauseOnHover;
            _manualAdvance = manualAdvance;
            _prevText = prevText;
            _nextText = nextText;
            _randomStart = randomStart;
            _beforeChange = beforeChange;
            _afterChange = afterChange;
            _slideshowEnd = slideshowEnd;
            _lastSlide = lastSlide;
            _afterLoad = afterLoad;
        }
        public IHtmlString GetHtml()
        {
            var thm = "theme-" + _theme.ToString().ToLower();
            var sb = new StringBuilder();
            sb.AppendLine("<div style='width:" + _width + ";height:auto;margin:0px auto' class='" + thm + "'>");
            sb.AppendLine("<div id='" + _id + "' class='nivoSlider'>");
            foreach (var model in _models)
            {
                string img = string.Format("<img src='{0}' alt='{1}' title='{2}' />", model.ImageFilePath, "", model.Caption);
                string item = "";
                if (model.LinkUrl.Trim().Length > 0 &&
                    Uri.IsWellFormedUriString(model.LinkUrl, UriKind.RelativeOrAbsolute))
                {
                    item = string.Format("<a href='{0}'>{1}</a>", model.LinkUrl, img);
                }
                else
                {
                    item = img;
                }
                sb.AppendLine(item);
            }
            sb.AppendLine("</div>");
            sb.AppendLine("</div>");

            sb.AppendLine("<script type='text/javascript'>");
            //sb.AppendLine("$('#" + _id + "').parent().ready(function () {");
            sb.Append("$(document).ready(function(){");
            sb.AppendLine("$('#" + _id + "').nivoSlider(" + makeParameters() + ");");
            //sb.AppendLine("$('.nivo-controlNav a').empty();");//semi hack for rtl layout
            //sb.AppendLine("$('#" + _id + "').nivoSlider();");
            sb.AppendLine("});");
            sb.AppendLine("</script>");

            return new HtmlString(sb.ToString());
        }
    }
    public enum NivoSliderTheme
    {
        Default, Light, Dark, Bar,
    }
    public class NivoSliderItem
    {
        public string ImageFilePath { get; set; }
        public string Caption { get; set; }
        public string LinkUrl { get; set; }
    }
6-سپس برای استفاده از این helper یک کنترلر به پروژه اضافه میکنیم مانند Home :
 public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }
        public virtual ActionResult NivoSlider()
        {
            var models = new List<NivoSliderItem>
            {
                new NivoSliderItem()
                    {
                        ImageFilePath =Url.Content("~/Images/img1.jpg"),
                        Caption = "عنوان اول",
                        LinkUrl = "http://www.google.com",
                    },
                new NivoSliderItem()
                    {
                        ImageFilePath =Url.Content("~/Images/img2.jpg"),
                        Caption = "#htmlcaption",
                        LinkUrl = "",
                    },
                new NivoSliderItem()
                    {
                        ImageFilePath =Url.Content("~/Images/img3.jpg"),
                        Caption = "عنوان سوم",
                        LinkUrl = "",
                    },
                new NivoSliderItem()
                    {
                        ImageFilePath =Url.Content("~/Images/img4.jpg"),
                        Caption = "عنوان چهارم",
                        LinkUrl = "",
                    },
                new NivoSliderItem()
                    {
                        ImageFilePath =Url.Content("~/Images/img5.jpg"),
                        Caption = "عنوان پنجم",
                        LinkUrl = "",
                    }
            };
            return PartialView("_NivoSlider", models);
        }
    }
7- سپس ویوی Home را نیز ایجاد میکنیم:
@{
    ViewBag.Title = "Index";
}

<h2>Nivo Slider Index</h2>

@{ Html.RenderAction("NivoSlider", "Home");}
8- یک پارشال ویو نیز برای رندر کردن NivoSliderهای خود ایجاد میکنیم:
@using NivoSlider.Helper
@model List<NivoSliderItem>
@{
    var nivo1 = new NivoSliderHelper("nivo1", Model.Skip(0).Take(3).ToList(), theme: NivoSliderTheme.Default, width: "500px");
    var nivo2 = new NivoSliderHelper("nivo2", Model.Skip(3).Take(2).ToList(), theme: NivoSliderTheme.Light, width: "500px");
}
@nivo1.GetHtml()

<div id="htmlcaption">
    <strong>This</strong> is an example of a <em>HTML</em> caption with <a href="#">a link</a>.
</div>

<br />
<br />
@nivo2.GetHtml()
نکته اول: اگر بخواهیم بر روی تصویر موردنظر متنی نمایش دهیم کافیست خاصیت Caption را مقداردهی کنیم. برای نمایش توضیحاتی پیچیده‌تر (مانند نمایش یک div و لینک و غیره) بعنوان توضیحات یک تصویر خاص باید خاصیت Caption را بصورت "{نام عنصر}#" مقداردهی کنیم و ویژگی class  مربوط به عنصر موردنظر را با nivo-html-caption مقداردهی کنیم.(همانند مثال بالا)
نکته دوم: این کامپوننت به همراه 4 تم (deafult- light- bar- dark) ارائه شده است. موقع استفاده از تم‌های دیگر باید رفرنس مربوط به آن تم را نیز اضافه کنید.
امیدوارم مفید واقع شود.
نظرات مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- ابتدا باید هدر مورد انتظار را مشخص کنید:
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
   services.AddAntiforgery(options => options.HeaderName = "__RequestVerificationToken"); 
}
- بعد متدی را برای تولید توکن در فایل razor درج کنید:
@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Xsrf

@functions{
    public string GetAntiXsrfRequestToken()
    {
        return Xsrf.GetAndStoreTokens(Context).RequestToken;
    }
}
- سپس اگر از jQuery استفاده می‌کنید، به صورت زیر این هدر با هر درخواستی به سمت سرور ارسال می‌شود:
<script type="text/javascript">
  $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
      jqXHR.setRequestHeader("__RequestVerificationToken", '@GetAntiXsrfRequestToken()');
  });
</script>
نظرات مطالب
نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Tooltip به کمک Twitter bootstrap
با سلام
چرا بعد از عملیات ajax ایی دیگه کار نمیده؟
از کد زیر برای نمایش استفاده شده:
<script type="text/javascript">
    $(document).ready(function () {
        $("[rel='tooltip']").tooltip({ placement: 'top', trigger: 'hover' });
    });
</script>

ولی بعد از انجام این دستور دیگه کار نمیده:
@Ajax.ActionLink(" ", MVC.Admin.ContactUs.ActionNames.List, MVC.Admin.ContactUs.Name,
            new
            {
                bywriter = ViewBag.bywriter,
                bydate = ViewBag.bydate,
                byisread = ViewBag.byisread,
                byisshow = ViewBag.byisshow,
                page = max,
                count = ViewBag.COUNT
            },
                new AjaxOptions
                {
                    HttpMethod = "Post",
                    InsertionMode = InsertionMode.Replace,
                    OnBegin = "showLoading",
                    UpdateTargetId = "listdiv",
                    OnComplete = "hideLoading"
                },
            new { @class = "glyphicon glyphicon-backward nodecoration", @rel = "tooltip", @title = "صفحه آخر" })
مطالب
انجام عملیات طولانی مدت با Web Workers
امروزه استفاده از صفحات وب، در همه امور به خوبی به چشم می‌خورد و تاثیر این فناوری را می‌توان در تمام عرصه‌های تولید و استفاده از نرم افزار دید. web worker یکی از فناوری‌های تحت وب بوده که توسط W3C ارائه شده است. وب ورکر به شما اجازه می‌دهد تا بتوانید عملیاتی را که نیاز به زمان زیادی برای پردازش دارد، در پشت صحنه انجام دهید؛ بدون اینکه وقفه‌ای در پردازش UI ایجاد شود. وب ورکر حتی به شما اجازه می‌دهد چند thread را همزمان اجرا کنید و پردازش‌هایی موازی یکدیگر داشته باشید. از آنجا که وب ورکرها یک ترد پردازشی جدا از UI به حساب می‌آیند، شما دسترسی به DOM ندارید؛ ولی می‌توانید از طریق ارسال پیام، با صفحه وب تعامل داشته باشد.

قبل از استفاده از وب ورکر، بهتر هست مرورگر را بررسی کنیم که آیا از این قابلیت پشتیبانی می‌کند یا خیر؟ روش بررسی کردن این قابلیت، شیوه‌های مختلفی دارد که به تعدادی از آن‌ها اشاره می‌کنیم:
typeof(Worker) !== "undefined"

 <script src="/js/modernizr-1.5.min.js"></script>
Modernizr.webworkers
هر کدام از عبارات بالا را اگر در شرطی بگذارید و جواب true بازگردانند به معنی پشتیبانی مرورگر این ویژگی است. modernizr فریمورکی جهت بررسی قابلیت‌های موجود در مرورگر است.
نحوه پشتیبانی وب ورکرها در مروگرهای مختلف به شرح زیر است:

برای ایجاد یک وب ورکر ابتدا لازم است تا کدهای پردازشی را داخل یک فایل js جداگانه بنویسیم. در این مثال ما قصد داریم که شمارنده‌ای را بنویسیم:
var i=0;

function timedCount() {
    i=i+1;
    postMessage(i);
    setTimeout("timedCount()", 500);
}

timedCount();

سپس در فایل HTML به شکل زیر وب ورکر را مورد استفاده قرار می‌دهیم. در سازنده Worker، ما آدرس فایل js را وارد می‌کنیم و برای توقف آن نیز از متد terminate استفاده می‌کنیم:
<!DOCTYPE html>
<html>
  <head>
    <script>
    var worker;

    function Start()
    {
      worker=new Worker("webwroker-even-numbers.js");
      worker.onmessage=(event)=>
      {
        document.getElementById("output").value=event.data;
      }
    }
    function Stop()
    {
      worker.terminate();
    }
    </script>
    <meta charset="utf-8">
    <title></title>
  </head>
  <body>
    <input type="text" id="output"/>
    <button onclick="Start();">Start Worker</button>
<button onclick="Stop();">Stop Worker</button>
  </body>
</html>
در صورتی که خطایی در ورکر رخ بدهد، می‌توانید از طریق متد onerror آن را دریافت کنید:
      worker.onerror = function (event) {
            console.log(event.message, event);
         };
مقدار برگشتی event شامل اطلاعات زیادی در مورد خطاست که شامل نام و مسیر فایل خطا، شماره خط و شماره ستون خطا، پیام خطا و ... می‌شود.
همچنین برای ورکر هم می‌توانید پیامی را ارسال کنید، برای همین کد زیر را به کد ورکر اضافه می‌کنیم:
self.onmessage=(event)=>{
  i=event.data;
};
و در صفحه HTML هم کد دریافت پیام از ورکر را به شکل زیر تغییر میدهیم:
  worker.onmessage=(event)=>
      {
        document.getElementById("output").value=event.data;
        if(event.data==8)
        worker.postMessage(100);
      }
در این حالت اگر عدد به هشت برسد، ما به ورکر می‌گوییم که عدد را به صد تغییر بده.

روش‌های ارسال پیام
به این نوع ارسال پیام، Structure Cloning گویند و با استفاده از این شیوه، امکان ارسال نوع‌های مختلفی امکان پذیر شده است؛ مثل فایل‌ها، Blob‌ها، آرایه‌ها و کلاس‌ها و ... ولی باید دقت داشته باشید که این ارسال پیام‌ها به صورت کپی بوده و آدرسی ارجاع داده نمی‌شود و باید مدنظر داشته باشید که ارسال یک فایل، به فرض پنجاه مگابایتی، به خوبی قابل تشخیص است. طبق نظر گوگل، از حجم 32 مگابایت به بعد، این گفته به خوبی مشهود بوده و زمانبر می‌شود. به همین علت فناوری با نام Transferable Objects ایجاد شده است که "کپی صفر" Zero-Copy را پیاده سازی می‌کند و باعث بهبود عملگر کپی می‌شود:
worker.postMessage(arrayBuffer, [arrayBuffer]);
 پارامتر اول آن، آرایه بافر شده است و دومی هم لیست آیتم‌هایی است که قرار است انتقال یابند:
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
if (ab.byteLength) {
  alert('Transferables are not supported in your browser!');
} else {
  // Transferables are supported.
}
یا ارسال اطلاعات بیشتر:
worker.postMessage({data: ab1, moreData: ab2},
                   [ab1, ab2]);
در صورتیکه بتواند انتقال را انجام بدهد، byteLength حجم اطلاعات ارسالی را بر می‌گرداند؛ در غیر اینصورت عدد 0 را به عنوان خروجی بر می‌گرداند. در این پرسش و پاسخ می‌توانید نمونه یک انتقال و دریافت را در این روش، ببینید.
نمودار زیر مقایسه‌ای بین Structure Cloning و Transferable Objects است که توسط گوگل منتشر شده است:

RTT=Round Trip Time 

نمودار بالا برای یک فایل 32 مگابایتی است که زمان رفت به ورکر و پاسخ (برگشت از ورکر) را اندازه گرفته‌اند. در ستون‌های اول، این موضوع برای فایرفاکس به روش Structure Cloning  به مدت 302 میلی ثانیه زمان برد که همین موضوع برای Transferables حدود 6.6 میلی ثانیه زمان برد.

آقای اریک بایدلمن در بخش مهندسی کروم گوگل می‌گوید: همین سرعت به ما در انتقال تکسچرها و مش‌ها در WebGL کمک می‌کند.


استفاده از اسکریپت خارجی

در صورتیکه قصد دارید از یک اسکرپیت خارجی، در ورکر استفاده کنید، تابع importScripts برای اینکار ایجاد شده است:

importScripts('script1.js');
importScripts('script2.js');
که البته به طور خلاصه‌تری نیز می‌توان نوشت:
importScripts('script1.js', 'script2.js');

Inline Worker
اگر بخواهید در همان صفحه اصلی یک ورکر را ایجاد کنید و فایل جاوا اسکریپتی خارجی نداشته باشید، می‌توانید از inline worker استفاده کنید. در این روش باید یک نوع blob را ایجاد کنید:
var blob = new Blob([
    "onmessage = function(e) { postMessage('msg from worker'); }"]);

// یک آدرس همانند آدرس ارجاع به فایل درست میکند
var blobURL = window.URL.createObjectURL(blob);

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  // e.data...
};
worker.postMessage(); // ورکر آغاز می‌شود
کاری که متد حیرت انگیز createObjectURL انجام می‌دهد این است که از داده‌های ذخیره شده در یک blob یا نوع فایل، یک آدرس ارجاعی شبیه آدرس زیر را ایجاد می‌کند:
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
آدرس‌هایی که این متد تولید می‌کند، یکتا بوده و تا پایان عمر صفحه، اعتبار دارند. به همین دلیل هر موقع به آن‌ها نیاز نداشتید، از دست آن‌ها خلاص شوید، تا حافظه به هدر نرود. برای آزادسازی حافظه می‌توان دستور زیر را به کار برد:
window.URL.revokeObjectURL(blobURL);
مرورگر کروم با دستور زیر به شما اجازه می‌دهد همه آدرس‌های blob‌ها را ببینید:
chrome://blob-internals
مطالب
Angular CLI - قسمت پنجم - ساخت و توزیع برنامه
ساخت و توزیع برنامه‌های Angular یکی از مهم‌ترین و بحث برانگیزترین قسمت‌های نگارش‌های جدید آن است و به ازای هر پروژه و قالبی که برای آن توسط گروه‌های مختلف ارائه شده‌است، روش‌های متفاوتی را شاهد خواهید بود. در ادامه روش توصیه شده‌ی توسط تیم Angular را که مبتنی است بر webpack و به صورت خودکار توسط Angular CLI مدیریت می‌شود، بررسی خواهیم کرد.


ساخت (Build) برنامه‌های Angular

Angular CLI کار ساخت و کامپایل برنامه را به صورت خودکار انجام داده و خروجی را در مسیری مشخص درج می‌کند. در اینجا می‌توان گزینه‌هایی را بر اساس نوع کامپایل مدنظر مانند کامپایل برای حالت توسعه و یا کامپایل برای حالت توزیع نهایی، انتخاب کرد. همچنین مباحث bundling و یکی کردن تعداد بالای ماژول‌های برنامه در آن لحاظ می‌شوند تا برنامه در حالت توزیع نهایی، سبب 100ها رفت و برگشت به سرور برای دریافت ماژول‌های مختلف آن نشود. به علاوه مباحث uglification (به نوعی obfuscation کدهای جاوا اسکریپتی نهایی) و tree-shaking (حذف کدهایی که در برنامه استفاده نشده‌اند؛ یا کدهای مرده) نیز پیاده سازی می‌شوند. با انجام tree-shaking‌، نه تنها اندازه‌ی توزیع نهایی به کاربر کاهش پیدا می‌کند، بلکه مرورگر نیز حجم کمتری از کدهای جاوااسکریپتی را باید تفسیر کند.
برای شروع می‌توان از دستور ذیل برای مشاهده‌ی تمام گزینه‌های مهیای ساخت برنامه استفاده کرد:
> ng build --help
ذکر تنهای دستور ng build‌، بدون هیچ گزینه‌ای، برای حالت «توسعه‌ی» برنامه بسیار ایده‌آل است (و دقیقا به معنای صدور دستور ng build --dev است). در این حالت خروجی کامپایل شده‌ی برنامه در پوشه‌ی dist تولید می‌شود. اگر از قسمت دوم این سری به خاطر داشته باشید، نام این پوشه‌ی خروجی، جزئی از تنظیمات فایل angular-cli.json. است:
"apps": [
{
   "outDir": "dist",
زمانیکه دستور ng build‌  صادر شود، این فایل‌ها را در پوشه‌ی dist خواهید یافت:

فایل 
توضیح 
 inline.bundle.js   WebPack runtime
از آن برای بارگذاری ماژول‌های برنامه و چسباندن قسمت‌های مختلف به یکدیگر استفاده می‌شود. 
 main.bundle.js   شامل تمام کدهای ما است. 
 polyfills.bundle.js   Polyfills - جهت پشتیبانی از مرورگرهای مختلف.
 styles.bundle.js    شامل بسته بندی تمام شیوه نامه‌های برنامه است 
vendor.bundle.js  کدهای کتابخانه‌های ثالث مورد استفاده و همچنین خود Angular، در اینجا بسته بندی می‌شوند. 
 

روشی برای بررسی محتوای bundleهای تولید شده

تولید bundleها در جهت کاهش رفت و برگشت‌های به سرور و بالا بردن کارآیی برنامه ضروری هستند؛ اما دقیقا این بسته بندی‌ها شامل چه اطلاعاتی می‌شوند؟ این اطلاعات را می‌توان از فایل‌های source map تولیدی استخراج کرد و برای این منظور می‌توان از برنامه‌ی source-map-explorer استفاده کرد.

روش نصب عمومی آن:
 > npm install -g source-map-explorer
روش اجرا:
 > source-map-explorer dist/main.bundle.js
پس از آن یک گزارش HTML ایی از محتوای bundle مدنظر تولید می‌شود.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت dev

در ادامه، کار Build همان مثالی را که در قسمت قبل توضیح داده شد، بررسی می‌کنیم. برای این منظور از طریق خط فرمان به ریشه‌ی پوشه‌ی اصلی پروژه وارد شده و دستور ng build را صادر کنید. یک چنین خروجی را مشاهده خواهید کرد:
 D:\Prog\angular-routing>ng build
Hash: 123cae8bd8e571f44c31
Time: 33862ms
chunk {0} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.bundle.js, main.bundle.js.map (main) 14.7 kB {3} [initial] [rendered]
chunk {2} styles.bundle.js, styles.bundle.js.map (styles) 9.77 kB {4} [initial] [rendered]
chunk {3} vendor.bundle.js, vendor.bundle.js.map (vendor) 2.34 MB [initial] [rendered]
chunk {4} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]
و اگر فایل index.html تولیدی آن‌را بررسی کنید، تنها الحاق همین 4 فایل js تولیدی را مشاهده می‌نمائید:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>AngularRouting</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root>Loading...</app-root>
<script type="text/javascript" src="inline.bundle.js">
</script><script type="text/javascript" src="polyfills.bundle.js">
</script><script type="text/javascript" src="styles.bundle.js">
</script><script type="text/javascript" src="vendor.bundle.js">
</script><script type="text/javascript" src="main.bundle.js"></script>
</body>
</html>

یک نکته: زمانیکه دستور ng serve -o صادر می‌شود، در پشت صحنه دقیقا همین دستور ng build صادر شده و اطلاعات را درون حافظه تشکیل می‌دهد. اما اگر کار ng build را دستی انجام دهیم، اینبار ng serve -o اطلاعات را از پوشه‌ی dist دریافت می‌کند. بنابراین در حین کار با ng serve -o نیازی به build دستی پروژه نیست.

سؤال: چرا حجم فایل endor.bundle.js اینقدر بالا است و شامل چه اجزایی می‌شود؟
نکته‌ای که در اینجا وجود دارد، حجم بالای فایل vendor.bundle.js آن است که 2.34 MB می‌باشد:


چون دستور ng build بدون پارامتری ذکر شده‌است، برنامه را برای حالت توسعه Build می‌کند و به همین جهت هیچگونه بهینه سازی در این مرحله صورت نخواهد گرفت. برای بررسی محتوای این فایل می‌توان دستور ذیل را در ریشه‌ی اصلی پروژه صادر کرد:
 > source-map-explorer dist/vendor.bundle.js
پس از اجرای این دستور، بلافاصله مرورگر پیش فرض سیستم اجرا شده و گزارشی را ارائه می‌دهد.


همانطور که مشاهده می‌کنید، در حالت بهینه سازی نشده و Build برای توسعه، کامپایلر Angular حدود 41 درصد حجم فایل vendor.bundle.js را تشکیل می‌دهد. به علاوه ماژول‌ها و قسمت‌هایی را ملاحظه می‌کنید که اساسا برنامه‌ی فعلی مثال ما از آن‌ها استفاده نمی‌کند؛ مانند http، فرم‌ها و غیره.


سفارشی سازی Build برای محیط‌های مختلف

اگر به پروژه‌ی تولید شده‌ی توسط Angular CLI دقت کنید، حاوی پوشه‌ای است به نام src\environments


هدف از فایل‌های environment برای نمونه تغییر آدرس توزیع برنامه در حالت توسعه و ارائه نهایی است.
همچنین در اینجا می‌توان نحوه‌ی بهینه سازی فایل‌های تولیدی را توسط Build Targets مشخص کرد و اینکار توسط ذکر پرچم prod-- (مخفف production) صورت می‌گیرد.
در ادامه، تفاوت‌های دستورهای ng build و ng build --prod را ملاحظه می‌کنید:
- با اجرای ng build، از فایل environment.ts استفاده می‌شود؛ برخلاف حالت اجرای ng build --prod که از فایل environment.prod.ts استفاده می‌کند.
- Cache-busting در حالت ارائه‌ی نهایی، به تمام اجزای پروژه اعمال می‌شود؛ اما در حالت توسعه فقط برای تصاویر قید شده‌ی در فایل‌های css.
- فایل‌های source map فقط برای حالت توسعه تولید می‌شوند.
- در حالت توسعه، cssها داخل فایل‌های js تولیدی قرار می‌گیرند؛ اما در حالت ارائه‌ی نهایی به صورت فایل‌های css بسته بندی می‌شوند.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار uglification انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار tree-shaking یا حذف کدهای مرده و بدون ارجاع، انجام نمی‌شود.
- در حالت توسعه برخلاف حالت ارائه‌ی نهایی، کار AOT انجام نمی‌شود. در اینجا AOT به معنای Ahead of time compilation است.
- در هر دو حالت توسعه و ارائه‌ی نهایی کار bundling و دسته بندی فایل‌ها انجام خواهد شد.

به همین جهت است که ng build سریع است؛ اما حجم بالاتری را هم تولید می‌کند. چون بسیاری از بهینه سازی‌های حالت ارائه‌ی نهایی را به همراه ندارد.


دستورات build برای حالت توسعه و ارائه‌ی نهایی

برای حالت توسعه، هر 4 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build متداول‌تر است:
>ng build --target=development --environment=dev
>ng build --dev -e=dev
>ng build --dev
>ng build

برای حالت ارائه‌ی نهایی، هر 3 دستور ذیل یک مفهوم را دارند و به همین جهت مورد ng build --prod متداول‌تر است:
>ng build --target=production --environment=prod
>ng build --prod -e=prod
>ng build --prod

همچنین هر کدام از این دستورات را توسط پرچم‌های ذیل نیز می‌توان سفارشی سازی کرد:

 پرچم  مخفف  توضیح
 sourcemap--  sm-  تولید سورس‌مپ
aot--    Ahead of Time compilation 
watch--  w-  تحت نظر قرار دادن فایل‌ها و ساخت مجدد
environment--  e-  محیط ساخت
 target--  t-  نوع ساخت
 dev--    مخفف نوع ساخت جهت توسعه
 prod--     مخفف نوع ساخت جهت ارائه نهایی

برای مثال در حالت prod، سورس‌مپ‌ها تولید نخواهند شد. اگر علاقمندید تا این فایل‌ها نیز تولید شوند، پرچم souremap را نیز ذکر کنید.
و یا اگر برای حالت dev می‌خواهید AOT را فعالسازی کنید، پرچم aot-- را در آنجا قید کنید.


یک مثال: ساخت برنامه‌ی مثال قسمت چهارم - تنظیمات مسیریابی در حالت prod

تا اینجا خروجی حالت dev ساخت برنامه‌ی قسمت چهارم را بررسی کردیم. در ادامه دستور ng build --prod را در ریشه‌ی پروژه صادر می‌کنیم:
 D:\Prog\angular-routing>ng build --prod
Hash: f5bd7fd555a85af8a86f
Time: 39932ms
chunk {0} polyfills.18173234f9641113b9fe.bundle.js (polyfills) 158 kB {4} [initial] [rendered]
chunk {1} main.c6958def7c5f51c45261.bundle.js (main) 50.3 kB {3} [initial] [rendered]
chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 69 bytes {4} [initial] [rendered]
chunk {3} vendor.b426ba6883193375121e.bundle.js (vendor) 1.37 MB [initial] [rendered]
chunk {4} inline.8cec210370dd3af5f1a0.bundle.js (inline) 0 bytes [entry] [rendered]


همانطور که ملاحظه می‌کنید، اینبار نه تنها حجم فایل‌ها به میزان قابل ملاحظه‌ای کاهش پیدا کرده‌اند، بلکه این نام‌ها به همراه یک سری hash هم هستند که کار cache-busting (منقضی کردن کش مرورگر، با ارائه‌ی نگارشی جدید) را انجام می‌دهند.

در ادامه اگر بخواهیم مجددا برنامه‌ی source-map-explorer را جهت بررسی محتوای فایل‌های js اجرا کنیم، به خطای عدم وجود sourcemapها خواهیم رسید (چون در حالت prod، به صورت پیش فرض غیرفعال هستند). به همین‌جهت برای این مقصود خاص نیاز است از پرچم فعالسازی موقت آن استفاده کرد:
> ng build --prod --sourcemap
> source-map-explorer dist/vendor.b426ba6883193375121e.bundle.js


همانطور که در تصویر نیز مشخص است، اینبار کامپایلر Angular به همراه تمام ماژول‌هایی که در برنامه ارجاعی به آن‌ها وجود نداشته‌است، حذف شده‌اند و کل حجم بسته‌ی Angular به 366 KB کاهش یافته‌است.


بررسی دستور ng serve

تا اینجا برای اجرای برنامه در حالت dev از دستور ng serve -o استفاده کرده‌ایم. کار ارائه‌ی برنامه توسط این دستور، از محتوای کامپایل شده‌ی درون حافظه با مدیریت webpack انجام می‌شود. به همین جهت بسیار سریع بوده و قابلیت live reload را ارائه می‌دهد (نمایش آنی تغییرات در مرورگر، با تغییر فایل‌ها).
همانند تمام دستورات دیگر، اطلاعات بیشتری را در مورد این دستور، از طریق راهنمای آن می‌توان به دست آورد:
 > ng serve --help

که شامل این موارد هستند (علاوه بر تمام مواردی را که در حالت ng build می‌توان مشخص کرد؛ مثلا ng serve --prod -o):

 پرچم مخفف
توضیح
 open-- o-
بازکردن خودکار مرورگر پیش فرض.
حالت پیش فرض آن گشودن مرورگر توسط خودتان است و سپس مراجعه‌ی دستی به آدرس برنامه. 
 port--  p-  تغییر پورت پیش فرض مانند ng server -p 8626 
 live-reload--  lr-   فعال است مگر اینکه آن‌را با false مقدار دهی کنید.
 ssl--    ارائه به صورت HTTPS
 proxy-config--  pc-  Proxy configuration file 


استخراج فایل تنظیمات webpack از Angular CLI

Angular CLI برای مدیریت build، در پشت صحنه از webpack استفاده می‌کند. فایل تنظیمات آن نیز جزئی از فایل‌های توکار این ابزار است و قرار نیست به صورت پیش فرض و مستقیم توسط پروژه‌ی جاری ویرایش شود. به همین جهت آن‌را در ساختار پروژه‌ی تولید شده، مشاهده نمی‌کنید.
اگر علاقمند به سفارشی سازی بیشتر این تنظیمات پیش فرض باشید، ابتدا باید آن‌را اصطلاحا eject کنید و سپس می‌توان آن‌را ویرایش کرد:
 > ng eject
Ejection was successful.

To run your builds, you now need to do the following commands:
- "npm run build" to build.
- "npm run test" to run unit tests.
- "npm start" to serve the app using webpack-dev-server.
- "npm run e2e" to run protractor.

Running the equivalent CLI commands will result in an error.
============================================
Some packages were added. Please run "npm install".
همانطور که مشاهده می‌کنید عنوان کرده‌است که از این پس خودتان باید بسیاری از مسایل را به صورت دستی مدیریت کنید و Angular CLI دیگر آن‌ها را به صورت خودکار مدیریت نمی‌کند و دیگر دستورات ng build و ng serve کار نخواهند کرد (این تغییرات در فایل package.json درج می‌شوند).
در این حالت است که فایل webpack.config.js به ریشه‌ی پروژه جهت سفارشی سازی شما اضافه خواهد شد. همچنین فایل‌های .angular-cli.json، package.json نیز جهت درج این تغییرات ویرایش می‌شوند.

و اگر در این لحظه پشیمان شده‌اید (!) فقط کافی است تا این مرحله‌ی جدید commit شده‌ی به مخزن کد را لغو کنید و باز هم به همان Angular CLI قبلی می‌رسید.
بازخوردهای دوره
مدیریت نگاشت ConnectionIdها در SignalR به کاربران واقعی سیستم
واقعا سپاسگذارم . ولی مشکلم و حل نمیکنه این موارد.
یکم گیج و سردرگم شدم.
ببینید من یه هاب ایجاد کردم به شکل زیر
 public class User
    {
        public string UserName { get; set; }
        public bool IsRole { get; set; }
        public HashSet<string> ConnectionIds { get; set; }
    }

    [Authorize]
    [HubName("userActivityHub")]
    public class UserActivityHub : Hub
    {
        private static readonly ConcurrentDictionary<string, User> Users = new ConcurrentDictionary<string, User>();
     

        public void AdminJoin()
        {
            Groups.Add(Context.ConnectionId, "admins");
        }

        public void Join()
        {
            var userName = Context.User.Identity.Name;
            var connectionId = Context.ConnectionId;
            var isAdmin = Context.User.IsInRole("Admin");

            var user = Users.GetOrAdd(userName, _ => new User
            {
                UserName = userName,
                IsRole = isAdmin,
                ConnectionIds = new HashSet<string>()
            });

            if (user.IsRole == true)
            {
                Groups.Add(user.ConnectionIds.ToString(), "admins");
            }
            else
            {
                lock (user.ConnectionIds)
                {
                    user.ConnectionIds.Add(connectionId);
                }
                Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
            }

        }

        public void GetUserCount()
        {
            Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
        }

        public override System.Threading.Tasks.Task OnDisconnected(bool stopCalled)
        {
            if (stopCalled)
            {
                var userName = Context.User.Identity.Name;
                var connectionId = Context.ConnectionId;

                User user;
                Users.TryGetValue(userName, out user);
                if (user != null)
                {
                    lock (user.ConnectionIds)
                    {
                        user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
                        if (!user.ConnectionIds.Any())
                        {
                            User removeUser;
                            Users.TryRemove(userName, out removeUser);
                        }
                    }
                }
                return Clients.Group("admins").showUserCount(Users.Count(a => a.Value.IsRole != true));
            }
            else
            {
                return base.OnDisconnected(false);
            }
        }
    }

اینک کد ویو Index
 <script type="text/javascript">
        var userHub = $.connection.userActivityHub;
        $.connection.hub.logging = true;
        $.connection.hub.start().done(function() {
            userHub.server.join();
        });

        $(function() {
            window.onbeforeunload = function() {
                $.connection.hub.stop();
            };
        });
    </script>

اینم ویو Index مربوط به Area Admin 
    <script type="text/javascript">
        var userHub = $.connection.userActivityHub;
        userHub.client.showUserCount = function (message) {
            $('#userOnlineCount').html(message);
        };
        $.connection.hub.start().done(function() {
            userHub.server.adminJoin().done(function() {
                userHub.server.getUserCount();
            });
        });
    </script>
وقتی کاربر لاگین میکنه یه کانتر تو ویو ادمین دارم که بهش اضافه میشه و برای لاگین کاربران درست کار میکنه.
مشکلم اینه که وقتی کاربر دکمه خروج و میزنه یا مرورگر رو میبنده رویداد OnDisconnected اجرا نمیشه! در نتیجه کانتر موجود در ویو ادمین هم کم نمیشه.
اینم کد دکمه خروج که البته تو یه پارشال ویو وجود داره
<script>
    $('a.btn.btn-danger.btn-block').click(function(e) {
        e.preventDefault();
        $('#logoutForm').submit();
        $.connection.userActivityHub.connection.stop();
    });
    $(function() {
        window.onbeforeunload = function(e) {
            $.connection.hub.stop();
        };
    });

</script>
نمیدونم مشکل کجاست! آیا باید برای دکمه خروج هم یه متد تو هاب ایجاد کنم یا همون Ondisconnected کافیه!
واقعا ممنون از زحمات شما.
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت اول - نصب پیشنیازها
به روز رسانی: ارتقاء به نگارش «2.0.0rc.0 »

برای ارتقاء به نگارش RC0، این مراحل را باید طی کنید:
1) پیش از هر کاری، پوشه‌ی node_modules قدیمی خود را حذف کنید (با تمام محتوای آن).
2) به روز رسانی فایل package.json به صورت ذیل:
{
    "name": "asp-net-mvc5x-angular2x",
    "version": "1.0.0",
    "author": "DNT",
    "description": "",
    "scripts": {
        "postinstall": "typings install"
    },
    "license": "Apache-2.0",
    "dependencies": {
        "@angular/common": "^2.0.0-rc.0",
        "@angular/compiler": "^2.0.0-rc.0",
        "@angular/core": "^2.0.0-rc.0",
        "@angular/http": "2.0.0-rc.0",
        "@angular/router": "2.0.0-rc.0",
        "@angular/router-deprecated": "^2.0.0-rc.0",
        "@angular/platform-browser": "^2.0.0-rc.0",
        "@angular/platform-browser-dynamic": "^2.0.0-rc.0",
        "bootstrap": "^3.3.6",
        "es6-promise": "^3.1.2",
        "es6-shim": "^0.35.0",
        "jquery": "^2.2.3",
        "reflect-metadata": "^0.1.3",
        "rxjs": "^5.0.0-beta.6",
        "systemjs": "^0.19.27",
        "zone.js": "^0.6.12"
    },
    "devDependencies": {
        "typescript": "^1.8.9",
        "typings": "^0.8.1"
    },
    "repository": { }
}
به روز شده‌ی محتوای فوق، همیشه در آدرس مستندات npm packages موجود است.
3) افزودن فایلی به نام typings.json در ریشه‌ی پروژه؛ با این محتوا:
{
    "ambientDependencies": {
        "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd"
    }
}
این فایل توسط قسمت «postinstall» اسکریپت package.json نصب می‌شود. اما چون مسیر https://raw.githubusercontent.com قابل دسترسی نیست (از این طرف البته!)، موفق به دریافت آن نخواهید شد. بنابراین یک پوشه را به نام typings به ریشه‌ی سایت اضافه کنید و سپس فایل ذیل را به آن اضافه نمائید:
es6-shim.d.ts
بدون این فایل، کامپایلر TypeScript تعاریف ES 6 را مانند Map و Promise و امثال آن‌، نمی‌شناسد و پروژه را کامپایل نخواهد کرد.

اکنون یکبار فایل package.json را ذخیره کنید تا کار به روز رسانی بسته‌ها انجام شود. البته اگر بر روی این فایل کلیک راست کنید، در منوی ظاهر شده، گزینه‌ی restore packages هم موجود است.

4) پس از آن، چند تغییر جزئی ذیل باید در کدهای این سری، اعمال شوند:
هر جایی angular2 تعریف شده، اینبار می‌شود angular@. مثلا:
import { PipeTransform, Pipe } from '@angular/core';
فایل مسیریابی آن قرار است تغییرات کلی داشته باشد. این مورد به صورت ذیل تغییر نام یافته است:
import { RouteParams, Router } from '@angular/router-deprecated';
5) فایل main.ts (قسمت دوم) به صورت ذیل تغییر کرده‌است:
/// <reference path="../typings/es6-shim.d.ts" />
import { bootstrap } from '@angular/platform-browser-dynamic';

// Our main component
import { AppComponent } from "./app.component";

bootstrap(AppComponent);
6) تعاریف اسکریپت‌های Index.html سایت، اینبار به نحو ذیل تغییر کرده‌اند:
یک نکته: اگر می‌خواهید این تعاریف را در یک فایل razor، وارد کنید، چون @ به ابتدای پوشه‌ی angular2 اضافه شده (node_modules\@angular)، مشکل پردازشی razor را ایجاد خواهد کرد و باید escape شود. به همین جهت بجای @ بهتر است معادل آن را یعنی ("@")Html.Raw@   وارد کنید.
سپس ابتدا فایل systemjs.config.js را از اینجا دریافت کنید.
در ادامه مداخل جدید را هم در فایل index.html مثال رسمی آغازین آن بررسی کنید.

بنابراین، فایل systemjs.config.js را  به ریشه‌ی سایت اضافه کنید (از این جهت که مسیر بسته‌های node_modules را از ریشه‌ی سایت می‌خواند). سپس فایل Views\Shared\_Layout.cshtml را به نحو ذیل تغییر دهید:
<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>

    <link href="~/node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/>
    <link href="~/app/app.component.css" rel="stylesheet"/>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css"/>

    <!-- 1. Load libraries -->
    <!-- IE required polyfills, in this exact order -->
    <script src="~/node_modules/es6-shim/es6-shim.min.js"></script>

    <script src="~/node_modules/zone.js/dist/zone.js"></script>
    <script src="~/node_modules/reflect-metadata/Reflect.js"></script>
    <script src="~/node_modules/systemjs/dist/system.src.js"></script>

    <script src="~/systemjs.config.js"></script>

    <!-- 2. Configure SystemJS -->
    <script>
        System.import('app/main').then(null, console.error.bind(console));
    </script>
</head>
<body>
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>

    @RenderSection("Scripts", required: false)
</body>
</html>

خلاصه‌ی سریع این موارد
الف) تغییرات آخرین بسته‌های npm را از مستندات آن پیگیری و اعمال کنید. آخرین نگارش آن همیشه در اینجا قابل دسترسی است.
ب) تغییرات index.html، فایل main.ts و مداخل آغازین آن‌را از مثال آغازین رسمی آن پیگیری و اعمال کنید.
مطالب
شروع کار با ASP.NET Web API 2
HTTP تنها برای به خدمت گرفتن صفحات وب نیست. این پروتکل همچنین پلتفرمی قدرتمند برای ساختن API هایی است که سرویس‌ها و داده را در دسترس قرار می‌دهند. این پروتکل ساده، انعطاف پذیر و در همه جا حاضر است. هر پلتفرمی که فکرش را بتوانید بکنید کتابخانه ای برای HTTP دارد، بنابراین سرویس‌های HTTP می‌توانند بازه بسیار گسترده ای از کلاینت‌ها را پوشش دهند، مانند مرورگرها، دستگاه‌های موبایل و اپلیکیشن‌های مرسوم دسکتاپ.

ASP.NET Web API فریم ورکی برای ساختن API‌های وب بر روی فریم ورک دات نت است. در این مقاله با استفاده از این فریم ورک، API وبی خواهیم ساخت که لیستی از محصولات را بر می‌گرداند. صفحه وب کلاینت، با استفاده از jQuery نتایج را نمایش خواهد داد.


یک پروژه Web API بسازید

در ویژوال استودیو 2013 پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را "ProductsApp" انتخاب کنید.

در دیالوگ New ASP.NET Project قالب Empty را انتخاب کنید و در قسمت "Add folders and core references for" گزینه Web API را انتخاب نمایید.

می توانید از قالب Web API هم استفاده کنید. این قالب با استفاده از ASP.NET MVC صفحات راهنمای API را خواهد ساخت. در این مقاله از قالب Empty استفاده میکنیم تا تمرکز اصلی، روی خود فریم ورک Web API باشد. بطور کلی برای استفاده از این فریم ورک لازم نیست با ASP.NET MVC آشنایی داشته باشید.

افزودن یک مدل

یک مدل (model) آبجکتی است که داده اپلیکیشن شما را معرفی می‌کند. ASP.NET Web API می‌تواند بصورت خودکار مدل شما را به JSON, XML و برخی فرمت‌های دیگر مرتب (serialize) کند، و سپس داده مرتب شده را در بدنه پیام HTTP Response بنویسد. تا وقتی که یک کلاینت بتواند فرمت مرتب سازی داده‌ها را بخواند، می‌تواند آبجکت شما را deserialize کند. اکثر کلاینت‌ها می‌توانند XML یا JSON را تفسیر کنند. بعلاوه کلاینت‌ها می‌توانند فرمت مورد نظرشان را با تنظیم Accept header در پیام HTTP Request مشخص کنند.

بگذارید تا با ساختن مدلی ساده که یک محصول (product) را معرفی میکند شروع کنیم.

کلاس جدیدی در پوشه Models ایجاد کنید.

نام کلاس را به "Product" تغییر دهید، و خواص زیر را به آن اضافه کنید.

namespace ProductsApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

افزودن یک کنترلر

در Web API کنترلر‌ها آبجکت هایی هستند که درخواست‌های HTTP را مدیریت کرده و آنها را به اکشن متدها نگاشت می‌کنند. ما کنترلری خواهیم ساخت که می‌تواند لیستی از محصولات، یا محصولی بخصوص را بر اساس شناسه برگرداند. اگر از ASP.NET MVC استفاده کرده اید، با کنترلرها آشنا هستید. کنترلرهای Web API مشابه کنترلر‌های MVC هستند، با این تفاوت که بجای ارث بری از کلاس Controller از کلاس ApiController مشتق می‌شوند.

کنترلر جدیدی در پوشه Controllers ایجاد کنید.

در دیالوگ Add Scaffold گزینه Web API Controller - Empty را انتخاب کرده و روی Add کلیک کنید.

در دیالوگ Add Controller نام کنترلر را به "ProductsController" تغییر دهید و روی Add کلیک کنید.

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

using ProductsApp.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;

namespace ProductsApp.Controllers
{
    public class ProductsController : ApiController
    {
        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public IHttpActionResult GetProduct(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                return NotFound();
            }
            return Ok(product);
        }
}
برای اینکه مثال جاری را ساده نگاه داریم، محصولات مورد نظر در یک آرایه استاتیک ذخیره شده اند. مسلما در یک اپلیکیشن واقعی برای گرفتن لیست محصولات از دیتابیس یا منبع داده ای دیگر کوئری می‌گیرید.

کنترلر ما دو متد برای دریافت محصولات تعریف می‌کند:

  • متد GetAllProducts لیست تمام محصولات را در قالب یک <IEnumerable<Product بر می‌گرداند.
  • متد GetProductById سعی می‌کند محصولی را بر اساس شناسه تعیین شده پیدا کند.

همین! حالا یک Web API ساده دارید. هر یک از متدهای این کنترلر، به یک یا چند URI پاسخ می‌دهند:

 URI  Controller Method
api/products/  GetAllProducts
 api/products/id/  GetProductById

برای اطلاعات بیشتر درباره نحوه نگاشت درخواست‌های HTTP به اکشن متدها توسط Web API به این لینک مراجعه کنید.


فراخوانی Web API با جاوا اسکریپت و jQuery

در این قسمت یک صفحه HTML خواهیم ساخت که با استفاده از AJAX متدهای Web API را فراخوانی می‌کند. برای ارسال درخواست‌های آژاکسی و بروز رسانی صفحه بمنظور نمایش نتایج دریافتی از jQuery استفاده میکنیم.

در پنجره Solution Explorer روی نام پروژه کلیک راست کرده و گزینه Add, New Item را انتخاب کنید.

در دیالوگ Add New Item قالب HTML Page را انتخاب کنید و نام فایل را به "index.html" تغییر دهید.

حال محتوای این فایل را با لیست زیر جایگزین کنید.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Product App</title>
</head>
<body>

  <div>
    <h2>All Products</h2>
    <ul id="products" />
  </div>
  <div>
    <h2>Search by ID</h2>
    <input type="text" id="prodId" size="5" />
    <input type="button" value="Search" onclick="find();" />
    <p id="product" />
  </div>

  <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script>
  <script>
    var uri = 'api/products';

    $(document).ready(function () {
      // Send an AJAX request
      $.getJSON(uri)
          .done(function (data) {
            // On success, 'data' contains a list of products.
            $.each(data, function (key, item) {
              // Add a list item for the product.
              $('<li>', { text: formatItem(item) }).appendTo($('#products'));
            });
          });
    });

    function formatItem(item) {
      return item.Name + ': $' + item.Price;
    }

    function find() {
      var id = $('#prodId').val();
      $.getJSON(uri + '/' + id)
          .done(function (data) {
            $('#product').text(formatItem(data));
          })
          .fail(function (jqXHR, textStatus, err) {
            $('#product').text('Error: ' + err);
          });
    }
  </script>
</body>
</html>
راه‌های مختلفی برای گرفتن jQuery وجود دارد، در این مثال از Microsoft Ajax CDN استفاده شده. می‌توانید این کتابخانه را از http://jquery.com دانلود کنید و بصورت محلی استفاده کنید. همچنین قالب پروژه‌های  Web API این کتابخانه را به پروژه نیز اضافه می‌کنند.


گرفتن لیستی از محصولات

برای گرفتن لیستی از محصولات، یک درخواست HTTP GET به آدرس "api/products/" ارسال کنید.

تابع getJSON یک درخواست آژاکسی ارسال می‌کند. پاسخ دریافتی هم آرایه ای از آبجکت‌های JSON خواهد بود. تابع done در صورت موفقیت آمیز بودن درخواست، اجرا می‌شود. که در این صورت ما DOM را با اطلاعات محصولات بروز رسانی می‌کنیم.

$(document).ready(function () {
    // Send an AJAX request
    $.getJSON(apiUrl)
        .done(function (data) {
            // On success, 'data' contains a list of products.
            $.each(data, function (key, item) {
                // Add a list item for the product.
                $('<li>', { text: formatItem(item) }).appendTo($('#products'));
            });
        });
});

گرفتن محصولی مشخص

برای گرفتن یک محصول توسط شناسه (ID) آن کافی است یک درخواست HTTP GET به آدرس "api/products/id/" ارسال کنید.

function find() {
    var id = $('#prodId').val();
    $.getJSON(apiUrl + '/' + id)
        .done(function (data) {
            $('#product').text(formatItem(data));
        })
        .fail(function (jqXHR, textStatus, err) {
            $('#product').text('Error: ' + err);
        });
}
برای این کار هنوز از getJSON برای ارسال درخواست آژاکسی استفاده می‌کنیم، اما اینبار شناسه محصول را هم به آدرس درخواستی اضافه کرده ایم. پاسخ دریافتی از این درخواست، اطلاعات یک محصول با فرمت JSON است.


اجرای اپلیکیشن

اپلیکیشن را با F5 اجرا کنید. صفحه وب باز شده باید چیزی مشابه تصویر زیر باشد.

برای گرفتن محصولی مشخص، شناسه آن را وارد کنید و روی Search کلیک کنید.

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


استفاده از F12 برای مشاهده درخواست‌ها و پاسخ ها

هنگام کار با سرویس‌های HTTP، مشاهده‌ی درخواست‌های ارسال شده و پاسخ‌های دریافتی بسیار مفید است. برای اینکار می‌توانید از ابزار توسعه دهندگان وب استفاده کنید، که اکثر مرورگرهای مدرن، پیاده سازی خودشان را دارند. در اینترنت اکسپلورر می‌توانید با F12 به این ابزار دسترسی پیدا کنید. به برگه Network بروید و روی Start Capturing کلیک کنید. حالا صفحه وب را مجددا بارگذاری (reload) کنید. در این مرحله اینترنت اکسپلورر ترافیک HTTP بین مرورگر و سرور را تسخیر می‌کند. می‌توانید تمام ترافیک HTTP روی صفحه جاری را مشاهده کنید.

به دنبال آدرس نسبی "api/products/" بگردید و آن را انتخاب کنید. سپس روی Go to detailed view کلیک کنید تا جزئیات ترافیک را مشاهده کنید. در نمای جزئیات، می‌توانید header‌ها و بدنه درخواست‌ها و پاسخ‌ها را ببینید. مثلا اگر روی برگه Request headers کلیک کنید، خواهید دید که اپلیکیشن ما در Accept header داده‌ها را با فرمت "application/json" درخواست کرده است.

اگر روی برگه Response body کلیک کنید، می‌توانید ببینید چگونه لیست محصولات با فرمت JSON سریال شده است. همانطور که گفته شده مرورگرهای دیگر هم قابلیت‌های مشابهی دارند. یک ابزار مفید دیگر Fiddler است. با استفاده از این ابزار می‌توانید تمام ترافیک HTTP خود را مانیتور کرده، و همچنین درخواست‌های جدیدی بسازید که این امر کنترل کاملی روی HTTP headers به شما می‌دهد.


قدم‌های بعدی

  • برای یک مثال کامل از سرویس‌های HTTP که از عملیات POST,PUT و DELETE پشتیبانی می‌کند به این لینک مراجعه کنید.
  • برای اطلاعات بیشتر درباره طراحی واکنش گرا در کنار سرویس‌های HTTP به این لینک مراجعه کنید، که اپلیکیشن‌های تک صفحه ای (SPA) را بررسی می‌کند.
مطالب
آموزش PouchDB : قسمت دوم (شروع به کار)

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

مطمئن باشید بیشتر از 10 دقیقه وقت شمارا نخواهد گرفت !

نصب PouchDB

فایل index.html  را باز کنید و فایل‌های PouchDB را به آن اضافه کنید :

<script src="//cdn.jsdelivr.net/pouchdb/2.2.0/pouchdb.min.js"></script>
<script src="js/base.js"></script>
<script src="js/app.js"></script>

حالا PouchDB نصب شده و آماده به کار است . ( در نسخه نهایی بهتر است از فایل‌های local استفاده کنید )

ایجاد بانک اطلاعاتی

بقیه کارها باید در فایل app.js انجام شود . برای شروع باید بانک اطلاعاتی جدیدی را برای درج اطلاعات خودمان ایجاد کنیم . برای ایجاد بانک اطلاعاتی خیلی ساده یک instance جدید از آبجکت PouchDB می‌سازیم .

var db = new PouchDB('todos');
var remoteCouch = false;

نیازی نیست که برای بانک خود، نما (Schema) ایجاد کنید! بعد از اینکه اسم را مشخص کنید، بانک آماده به کار است.

ثبت اطلاعات در بانک اطلاعاتی

اولین کاری که باید انجام دهیم این است که یک متد را ایجاد کنیم و توسط آن اطلاعات خودمان را در بانک اطلاعاتی ذخیره کنیم. نام متد را addTodo انتخاب می‌کنیم و کارش این است که وقتی کاربر کلید Enter را فشار داد، اطلاعات را داخل بانک اطلاعاتی ذخیره کند. متد مورد نظر به این صورت هست:

function addTodo(text) {
  var todo = {
    _id: new Date().toISOString(),
    title: text,
    completed: false
  };
  db.put(todo, function callback(err, result) {
    if (!err) {
      console.log('Successfully posted a todo!');
    }
  });
}

در PouchDB هر پرونده نیاز دارد تا یک فیلد unique با نام _id داشته باشد. اگر داده‌ای بخواهد در بانک اطلاعاتی ثبت شود و دارای _id مشابه باشد، با آن به صورت یک update رفتار خواهد شد. در اینجا ما از تاریخ با عنوان id استفاده کردیم که در این مورد خاص می‌باشد. شما می‌تواند از db.post() نیز استفاده کنید؛ اگر یک id را به صورت random می‌خواهید. تنها چیزی که اجباری است در هنگام ساختن یک پرونده، همین _id است و بقیه موارد کاملا اختیاری هستند.

نمایش اطلاعات

در اینجا ما از یک function کمکی به نام redrawTodosUI استفاده خواهیم کرد که وظیفه‌اش این است تا یک array را دریافت کرده و آن را هر طور که مشخص کردید نمایش دهد. البته آن را به سلیقه خودتان می‌توانید آماده کنید.
تنها کاری که باید انجام دهیم این است که اطلاعات را از بانک اطلاعاتی استخراج کرده و به function مورد نظر پاس دهیم.
در اینجا می‌توانیم به سادگی از db.allDocs برای خواندن اطلاعات از بانک اطلاعاتی استفاده کنیم.
خاصیت include_docs به PouchDB این دستور را می‌دهد که ما درخواست دریافت همه اطلاعات داخل پرونده‌ها را داریم و descending هم نحوه مرتب سازی را که بر اساس _id هست، مشخص می‌کند تا بتوانیم جدیدترین اطلاعات را اول دریافت کنیم .

function showTodos() {
  db.allDocs({include_docs: true, descending: true}, function(err, doc) {
    redrawTodosUI(doc.rows);
  });
}

به روزرسانی خودکار

هر بار که ما اطلاعات جدیدی را در بانک اطلاعاتی وارد می‌کنیم، نیازمند این هستیم تا اطلاعات جدید را به صورت خودکار دریافت و نمایش دهیم. برای این منظور می‌توانیم به روش زیر عمل کنیم :

var remoteCouch = false;

db.info(function(err, info) {
  db.changes({
    since: info.update_seq,
    live: true
  }).on('change', showTodos);
});

حالا هر بار که اطلاعات جدیدی در بانک اطلاعات ثبت شود، به صورت خودکار نمایش داده خواهد شد. خاصیت live مشخص می‌کند که این کار می‌تواند بی نهایت بار انجام شود.

ویرایش اطلاعات

وقتی که کاربر یک آیتم Todo را کامل می‌کند، یک چک باکس را علامت می‌زند و اعلام می‌کند که این کار انجام شده است.

function checkboxChanged(todo, event) {
  todo.completed = event.target.checked;
  db.put(todo);
}

این بخش شبیه به قسمت ثبت اطلاعات است، با این تفاوت که باید شامل یک فیلد _rev ( اضافه بر _id ) نیز باشد. در غیر اینصورت تغییرات ثبت نخواهد شد. این کار برای این است که اشتباهی، اطلاعاتی در بانک ثبت نشود.

حذف اطلاعات

برای حذف اطلاعات باید از متد db.remove به همراه آبجکت مورد نظر استفاده کنید .

function deleteButtonPressed(todo) {
  db.remove(todo);
}

مانند بخش ویرایش نیز باید در اینجا هم _id و هم _rev مشخص شود.
باید توجه داشته باشید در اینجا همان آبجکتی را که از بانک فراخوانی کرده‌ایم، به این متد پاس می‌دهیم.
البته شما می‌تونید آبجکت خودتان را نیز ایجاد کنید {_id: todo._id, _rev: todo._rev} اما خوب همان روش قبلی عاقلانه‌تر است و احتمال خطای کمتری دارد .

نصب CouchDB

حالا می‌خواهیم همگام سازی را اجرا کنیم و برای این کار نیاز هست یا CouchDB را به صورت Local نصب کنیم یا از سرویس‌های آنلاین مثل IrisCouch استفاده کنید .

فعال سازی CORS

برای اینکه به صورت مستقیم با CouchDB در ارتباط باشید باید مطمئن شوید که CORS فعال هست.
در اینجا فقط نام کاربری و رمز ورود را مشخص کنید. به صورت پیش فرض CouchDB به صورت Admin Party نصب می‌شود و نیازی به User و password ندارد. مگر اینکه برایش مشخص کنید.
همچنین شما باید myname.iriscouch.com را با سرور خودتان جایگزین کنید که اگر به صورت local کار می‌کنید 127.0.0.1:5984 است.

$ export HOST=http://username:password@myname.iriscouch.com
$ curl -X PUT $HOST/_config/httpd/enable_cors -d '"true"'
$ curl -X PUT $HOST/_config/cors/origins -d '"*"'
$ curl -X PUT $HOST/_config/cors/credentials -d '"true"'
$ curl -X PUT $HOST/_config/cors/methods -d '"GET, PUT, POST, HEAD, DELETE"'
$ curl -X PUT $HOST/_config/cors/headers -d \
  '"accept, authorization, content-type, origin"'

راه اندازی ارتباط ساده دو طرفه

به فایل app.js برگردید . در اینجا باید آدرس بانک اطلاعاتی آنلاین را مشخص کنیم.

var db = new PouchDB('todos');
var remoteCouch = 'http://user:pass@mname.iriscouch.com/todos';

فراموش نکنید که نام کاربری و رمز ورود را بسته به نیاز خود تغییر دهید.
حالا می‌تونیم function که وظیفه همگام سازی اطلاعات را به عهده دارد بنویسیم :

function sync() {
  syncDom.setAttribute('data-sync-state', 'syncing');
  var opts = {live: true};
  db.replicate.to(remoteCouch, opts, syncError);
  db.replicate.from(remoteCouch, opts, syncError);
}

db.replicate() به PouchDB می‌گوید که که همه اطلاعات را "به" یا "از" remoteCouch انتقال دهد.
ما دوبار این درخواست را دادیم. در بار اول اطلاعات به داخل سرور ریموت منتقل می‌شود و در بار دوم اطلاعات local جایگزین می‌شوند.
یک callback هم وقتی که این کار به پایان برسد اجرا خواهد شد .
می‌توانید برنامه خود را در دو مرورگر مختلف اجرا کنید تا نتیجه کار را ببینید.

تبریک می‌گوییم !

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

مطالب
Kendo UI MVVM
پیشنیازها
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودی‌های کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوه‌ی تعریف DataSource ایی که می‌تواند اطلاعات را ثبت، حذف و یا ویرایش کند.


در این مطلب قصد داریم به یک چنین صفحه‌ای برسیم که در آن در ابتدای نمایش، لیست ثبت نام‌های موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده می‌شود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیف‌های جدید. در این بین مدیریت نمایش لیست ثبت نام‌ها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.



Kendo UI MVVM

الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترل‌های برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شده‌است، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانه‌هایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شده‌اند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز می‌باشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل می‌شود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده می‌شود.
- ViewModel واسطی است بین Model و View. کار آن انتقال داده‌ها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح می‌باشد.
- Declarative data binding جهت رهایی برنامه نویس‌ها از نوشتن کدهای هماهنگ سازی اطلاعات المان‌های View و خواص ViewModel کاربرد دارد.

در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال می‌کنیم.


تعریف Model و ViewModel

در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models
{
    public class Registration
    {
        public int Id { set; get; }
        public string UserName { set; get; }
        public string CourseName { set; get; }
        public int Credit { set; get; }
        public string Email { set; get; }
        public string Tel { set; get; }
    }
}
در سمت کاربر، این مدل را به نحو ذیل می‌توان تعریف کرد:
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                id: "Id",
                fields: {
                    Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new.
                    UserName: { type: 'string' },
                    CourseName: { type: 'string' },
                    Credit: { type: 'number' },
                    Email: { type: 'string' },
                    Tel: { type: 'string' }
                }
            });
        });
    </script>
و ViewModel برنامه در ساده‌ترین شکل آن اکنون چنین تعریفی را خواهد یافت:
    <script type="text/javascript">
        $(function () {
            var viewModel = kendo.observable({
                accepted: false,
                course: new model()
            });
        });
    </script>
یک viewModel در Kendo UI به صورت یک observable object تعریف می‌شود که می‌تواند دارای تعدادی خاصیت و متد دلخواه باشد. هر خاصیت آن به یک عنصر HTML متصل خواهد شد. در اینجا این اتصال دو طرفه است؛ به این معنا که تغییرات UI به خواص viewModel و برعکس منتقل و منعکس می‌شوند.


اتصال ViewModel به View برنامه

تعریف فرم ثبت نام را در اینجا ملاحظه می‌کنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگی‌های خاص آن، مزین شده‌اند. جزئیات آن‌را در مطلب «اعتبار سنجی ورودی‌های کاربر در Kendo UI» پیشتر بررسی کرده‌ایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                <h3>ثبت نام</h3>
                <ul>
                    <li>
                        <label for="Id">Id</label>
                        <span id="Id" data-bind="text:course.Id"></span>
                    </li>
                    <li>
                        <label for="UserName">نام</label>
                        <input type="text" id="UserName" name="UserName" class="k-textbox"
                               data-bind="value:course.UserName"
                               required />
                    </li>
                    <li>
                        <label for="CourseName">دوره</label>
                        <input type="text" dir="ltr" id="CourseName" name="CourseName" required
                               data-bind="value:course.CourseName" />
                        <span class="k-invalid-msg" data-for="CourseName"></span>
                    </li>
                    <li>
                        <label for="Credit">مبلغ پرداختی</label>
                        <input id="Credit" name="Credit" type="number" min="1000" max="6000"
                               required data-max-msg="عددی بین 1000 و 6000" dir="ltr"
                               data-bind="value:course.Credit"
                               class="k-textbox k-input" />
                        <span class="k-invalid-msg" data-for="Credit"></span>
                    </li>
                    <li>
                        <label for="Email">پست الکترونیک</label>
                        <input type="email" id="Email" dir="ltr" name="Email"
                               data-bind="value:course.Email"
                               required class="k-textbox" />
                    </li>
                    <li>
                        <label for="Tel">تلفن</label>
                        <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}"
                               required class="k-textbox"
                               data-bind="value:course.Tel"
                               data-pattern-msg="8 رقم" />
                    </li>
                    <li>
                        <input type="checkbox" name="Accept"
                               data-bind="checked:accepted"
                               required />
                        شرایط دوره را قبول دارم.
                        <span class="k-invalid-msg" data-for="Accept"></span>
                    </li>
                    <li>
                        <button class="k-button"
                                data-bind="enabled: accepted, click: doSave"
                                type="submit">
                            ارسال
                        </button>
                        <button class="k-button" data-bind="click: resetModel">از نو</button>
                    </li>
                </ul>
                <span id="doneMsg"></span>
            </form>
        </div>
برای اتصال ViewModel تعریف شده به ناحیه‌ی مشخص شده با DIV ایی با Id مساوی coursesSection، می‌توان از متد kendo.bind استفاده کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
            // ...
            });

            var viewModel = kendo.observable({
            // ...
            });

            kendo.bind($("#coursesSection"), viewModel);
        });
    </script>
به این ترتیب Kendo UI به بر اساس تعریف data-bind یک فیلد، برای مثال تغییرات خواص course.UserName را به text box نام کاربر منتقل می‌کند و همچنین اگر کاربر اطلاعاتی را در این text box وارد کند، بلافاصله این تغییرات در خاصیت course.UserName منعکس خواهند شد.
<input type="text" id="UserName" name="UserName" class="k-textbox"
       data-bind="value:course.UserName"
       required />

بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهله‌ای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترل‌های برنامه، تنها کافی است ویژگی‌های data-bind ایی را به آن‌ها اضافه کنیم.
در ناحیه‌ی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شده‌است (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کرده‌ایم:
<input type="checkbox" name="Accept"
       data-bind="checked:accepted"
       required />

<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
برای مثال اگر کاربر این checkbox را انتخاب کند، مقدار خاصیت accepted، مساوی true خواهد شد. تغییر مقدار این خاصیت، توسط ViewModel بلافاصله در کل ناحیه coursesSection منتشر می‌شود. به همین جهت ویژگی enabled: accepted که به معنای مقید بودن فعال یا غیرفعال بودن دکمه بر اساس مقدار خاصیت accepted است، دکمه را فعال می‌کند، یا برعکس و برای انجام این عملیات نیازی نیست کدنویسی خاصی را انجام داد. در اینجا بین checkbox و button یک سیم کشی برقرار است.


ارسال داده‌های تغییر کرده‌ی ViewModel به سرور

تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شده‌اند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المان‌های فرم متصل می‌کند نیز در ادامه اضافه شده‌است. توسط ویژگی‌های data-bind کار Declarative data binding انجام می‌شود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
    <script type="text/javascript">
        $(function () {
            var model = kendo.data.Model.define({
                //...
            });

            var dataSource = new kendo.data.DataSource({
                type: 'json',
                transport: {
                    read: {
                        url: "api/registrations",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/registrations",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (course) {
                            return "api/registrations/" + course.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (data, type) {
                        // Convert to a JSON string.  Without this step your content will be form encoded.
                        return JSON.stringify(data);
                    }
                },
                schema: {
                    model: model
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });

            var viewModel = kendo.observable({
                //...
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
در اینجا تعریف DataSource کار با منبع داده راه دور ASP.NET Web API را مشاهده می‌کنید. تعاریف اصلی آن با تعاریف مطرح شده در مطلب «فعال سازی عملیات CRUD در Kendo UI Grid» یکی هستند. هر قسمت آن مانند read، create، update و destory به یکی از متدهای کنترلر ASP.NET Web API اشاره می‌کنند. حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.


متصل کردن DataSource به ViewModel

تا اینجا DataSource ایی جهت کار با سرور تعریف شده‌است؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
    <script type="text/javascript">
        $(function () {
            $("#coursesSection").kendoValidator({
                // ...
            });

            var model = kendo.data.Model.define({
                // ...
            });

            var dataSource = new kendo.data.DataSource({
                // ...
            });

            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                doSave: function (e) {
                    e.preventDefault();
                    console.log("this", this.course);
                    var validator = $("#coursesSection").data("kendoValidator");
                    if (validator.validate()) {
                        if (this.course.Id == 0) {
                            dataSource.add(this.course);
                        }
                        dataSource.sync(); // push to the server
                        this.set("course", new model()); // reset controls
                    }
                },
                resetModel: function (e) {
                    e.preventDefault();
                    this.set("course", new model());
                }            
             });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
همانطور که در تعاریف تکمیلی viewModel مشاهده می‌کنید، اینبار دو متد جدید دلخواه doSave و resetModel را اضافه کرده‌ایم.
در متد doSave، ابتدا بررسی می‌کنیم آیا اعتبارسنجی فرم با موفقیت انجام شده‌است یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه می‌کنیم. در اینجا بررسی شده‌است که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت می‌گردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شده‌است.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن می‌توان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال می‌کند.
متد دلخواه  resetModel سبب مقدار دهی مجدد شیء course با یک وهله‌ی جدید از شیء model می‌شود. همینقدر برای پاک کردن تمامی کنترل‌های صفحه کافی است.

تا اینجا دو متد جدید را در ViewModel برنامه تعریف کرده‌ایم. در مورد نحوه‌ی اتصال آن‌ها به View، به کدهای دو دکمه‌ی موجود در فرم دقت کنید:
<button class="k-button"
        data-bind="enabled: accepted, click: doSave"
        type="submit">
       ارسال
</button>
<button class="k-button" data-bind="click: resetModel">از نو</button>
این متدها نیز توسط ویژگی‌های data-bind به هر دکمه نسبت داده شده‌اند. به این ترتیب برای مثال با کلیک کاربر بر روی دکمه‌ی submit، متد doSave موجود در ViewModel فراخوانی می‌شود.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی می‌گردد. نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است.  اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال می‌کند.
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using KendoUI07.Models;

namespace KendoUI07.Controllers
{
    public class RegistrationsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            RegistrationsDataSource.LatestRegistrations.Remove(item);
            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

        public IEnumerable<Registration> Get()
        {
            return RegistrationsDataSource.LatestRegistrations;
        }

        public HttpResponseMessage Post(Registration registration)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            registration.Id = id;
            RegistrationsDataSource.LatestRegistrations.Add(registration);

            // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود
            return Request.CreateResponse(HttpStatusCode.Created, registration);
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public HttpResponseMessage Update(int id, Registration registration)
        {
            var item = RegistrationsDataSource.LatestRegistrations
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);


            if (!ModelState.IsValid || id != registration.Id)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

            RegistrationsDataSource.LatestRegistrations[item.Index] = registration;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
در اینجا بیشتر امضای این متدها مهم هستند، تا منطق پیاده سازی شده در آن‌ها. همچنین بازگشت Id رکورد جدید، توسط متد Post نیز بسیار مهم است و سبب می‌شود تا DataSource بداند با فراخوانی متد sync آن، باید عملیات Post یا create انجام شود یا Put و update.


نمایش آنی اطلاعات ثبت شده در یک لیست

ردیف‌های اضافه شده به منبع داده را می‌توان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
    <div id="coursesSection" class="k-rtl k-header">
        <div class="box-col">
            <form id="myForm" data-role="validator" novalidate="novalidate">
                           <!--فرم بحث شده در ابتدای مطلب-->
            </form>
        </div>
        <div id="results">
            <table class="metrotable">
                <thead>
                    <tr>
                        <th>Id</th>
                        <th>نام</th>
                        <th>دوره</th>
                        <th>هزینه</th>
                        <th>ایمیل</th>
                        <th>تلفن</th>
                        <th></th>
                        <th></th>
                    </tr>
                </thead>
                <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody>
                <tfoot data-template="footer-template" data-bind="source: this"></tfoot>
            </table>
            <script id="row-template" type="text/x-kendo-template">
                <tr>
                    <td data-bind="text: Id"></td>
                    <td data-bind="text: UserName"></td>
                    <td dir="ltr" data-bind="text: CourseName"></td>
                    <td>
                        #: kendo.toString(get("Credit"), "c0") #
                    </td>
                    <td data-bind="text: Email"></td>
                    <td data-bind="text: Tel"></td>
                    <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td>
                    <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td>
                </tr>
            </script>
            <script id="footer-template" type="text/x-kendo-template">
                <tr>
                    <td colspan="3"></td>
                    <td>
                        جمع کل: #: kendo.toString(totalPrice(), "c0") #
                    </td>
                    <td colspan="2"></td>
                    <td></td>
                    <td></td>
                </tr>
            </script>
        </div>
    </div>
در ناحیه‌ی coursesSection که توسط متد kendo.bind به viewModel برنامه متصل شده‌است، یک جدول را برای نمایش ردیف‌های ثبت شده توسط کاربر اضافه کرده‌ایم. thead آن بیانگر سر ستون جدول است. قسمت tbody و tfoot این جدول توسط دو Kendo UI Template مقدار دهی شد‌ه‌اند. هر کدام نیز منبع داده‌اشان را از view model دریافت می‌کنند. در row-template معادل خواص شیء course را مشاهده می‌کنید. در footer-template متد totalPrice برای نمایش جمع ستون هزینه اضافه شده‌است. بنابراین مطابق این قسمت از View، به یک خاصیت جدید coursesDataSourceRows و سه متد deleteCourse، editCourse و totalPrice نیاز است:
    <script type="text/javascript">
        $(function () {
            // ...
            var viewModel = kendo.observable({
                accepted: false,
                course: new model(),
                coursesDataSourceRows: new kendo.data.ObservableArray([]),
                doSave: function (e) {
                       // ...
                },
                resetModel: function (e) {
                      // ...
                },
                totalPrice: function () {
                    var sum = 0;
                    $.each(this.get("coursesDataSourceRows"), function (index, item) {
                        sum += item.Credit;
                    });
                    return sum;
                },
                deleteCourse: function (e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    dataSource.remove(course);
                    dataSource.sync(); // push to the server
                },
                editCourse: function(e) {
                    // the current data item is passed as the "data" field of the event argument
                    var course = e.data;
                    this.set("course", course);
                }
            });

            kendo.bind($("#coursesSection"), viewModel);
            dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار
        });
    </script>
نحوه‌ی اتصال خاصیت جدید coursesDataSourceRows که به عنوان منبع داده ردیف‌های row-template عمل می‌کند، به این صورت است:
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه می‌شود تا در ناحیه‌ی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
    <script type="text/javascript">
        $(function () {
            var dataSource = new kendo.data.DataSource({
                //...
                change: function (e) {
                    // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی
                    viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view()));
                }
            });
        });
    </script>
متد change آن، هر زمانیکه اطلاعاتی در منبع داده تغییر کنند یا اطلاعاتی به سمت سرور ارسال یا دریافت گردد، فراخوانی می‌شود. در همینجا فرصت خواهیم داشت تا خاصیت coursesDataSourceRows را جهت نمایش اطلاعات موجود در منبع داده، مقدار دهی کنیم. همین مقدار دهی ساده سبب اجرای row-template برای تولید ردیف‌های جدول می‌شود. استفاده از new kendo.data.ObservableArray سبب خواهد شد تا اگر اطلاعاتی در فرم برنامه تغییر کند، این اطلاعات بلافاصله در لیست گزارش برنامه نیز منعکس گردد.



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