نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
ساختار نهایی متود DownloadFile در ImageHandlerController :

[AllowAnonymous]
        [ResponseCache(VaryByHeader = "id;imgSize", Duration = 30, Location = ResponseCacheLocation.Client, NoStore = true)]
        public IActionResult DownloadFile([FromRoute]string id, [FromQuery] ImgSize imgSize)
        {
         //Tuple<string, string, string>(queryResult.FileName, queryResult.FileOnDs,queryResult.FileContentType)   
          var result = _baseFileService.GetFileNameAndFileNameOnDsAndFileType(id);
            if (result == null) return View("Error");

            var fileName = result.Item1;
            string userAgent = Request.Headers["User-Agent"];
            if (IsInternetExplorer(userAgent))
            {
                var htencode = HtmlEncoder.Create();
                var attachment = string.Format("attachment; filename=\"{0}\"", htencode.Encode(fileName));
                _httpContext.HttpContext.Response.Headers.Add("Content-Disposition", attachment);
            }
            var rootPath = Path.Combine(_hostingEnvironment.WebRootPath, _settingsAppPathConfig.Value.ServerImagesRootPath);
            var filepath = Path.Combine(rootPath, imgSize.ToString().ToLower(), result.Item2);
            var filefinalpath = "~/" + _settingsAppPathConfig.Value.ServerImagesRootPath + "/" + imgSize.ToString().ToLower() + "/" + result.Item2;
            if (!System.IO.File.Exists(filepath))
            {
                const string notFoundImage = "notFound.jpg";
                var notFoundpath = "~/" + _settingsAppPathConfig.Value.ServerImagesRootPath + "/"+ notFoundImage;
                string contentType;
                new FileExtensionContentTypeProvider().TryGetContentType(notFoundImage, out contentType);
                return File(notFoundpath, contentType, notFoundImage);
            }
            string contentTypebase;
            new FileExtensionContentTypeProvider().TryGetContentType(result.Item3, out contentTypebase);
            return File(filefinalpath, contentTypebase, fileName);
        }
مطالب
نمایش خروجی RSS سایت‌های دیگر به کمک jQuery
شاید خیلی از دوستان (مثل گذشته نه چندان دور خودم ) خیلی بیش از اندازه به برنامه نویسی‌های سمت سرور اهمیت  می دهند که این کار باعث از دست دادن ، سرعت و سادگی برنامه نویسی  سمت کلاینت می‌شود.
معمولا ما برای کار با خروجی‌های XML از کد‌های سمت سرور استفاده می‌کنیم ، بدون اینکه از قدرت جی کوئری در این زمینه  اطلاعی داشته باشیم. البته در این مقاله خیلی به پردازش XML  توسط جی کوئری نمی‌پردازیم و کار اصلی را گوگل برای ما انجام می‌دهد.
برای انجام این کار ما ابتدا توسط یکی از کتابخانه‌های گوگل ، خروجی RSS یک وب سایت رامی خوانیم و بعد به کمک jQuery  آن‌ها را نمایش می‌دهیم.
    <script src="jquery-1.8.0.min.js" type="text/javascript"></script>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript">

        google.load("feeds", "1");

        function initializeda() {
            var feed = new google.feeds.Feed("http://www.drupaleasy.ir/rss.xml");
            feed.setNumEntries(5);
            feed.setResultFormat(google.feeds.Feed.JSON_FORMAT);
            feed.load(function (result) {
                if (!result.error) {
                    for (var i = 0; i < result.feed.entries.length; i++) {
                        var entry = result.feed.entries[i];
                        $('#drupaleasy ul').append('<li><a href="' + entry.link + '">' + entry.title + '</a></li>');
                    }
                }
            });
        }

google.setOnLoadCallback(initializeda);
    </script>
توضیحات کد :
ابتدا نیاز داریم کتابخانه گوگل را به صفحه اضافه کنیم. شما می‌توانید مستندات کامل این کتابخانه در این لینک  بخوانید.
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
در ابتدا معرفی می‌کنیم که به کدام امکان این کتابخانه نیاز داریم ، در این جا ما از ورژن "1" تابع feed  استفاده می‌کنیم.بد نیست برای آشنایی بیشتر سری هم به این لینک  بزنید.
google.load("feeds", "1");
ابتدا لینک مورد نظر که باید خروجی از آن گرفته شود را معرفی می‌کنیم و آن را در متغییری قرار می‌دهیم.
var feed = new google.feeds.Feed("http://www.drupaleasy.ir/rss.xml");
تنظیمات دلخواه خودمان را نظیر تعداد پست واکشی شده و نوع خروجی مشخص می‌کنیم. تجربه (شخصی )نشان داده که بهترین نوع خروجی JSON   است.
feed.setNumEntries(5);
feed.setResultFormat(google.feeds.Feed.JSON_FORMAT);
و بعد هم به کمک jQuery  ساختار مورد نظر خودمان را تنظیم می‌کنیم. شما می‌توانید به کمک مستندات ، و سلیقه شخصی خودتان این کار را انجام بدهید ، و در آخر هم تنظیم می‌کنید ، اجرای تابع شما در چه زمانی اتفاق بیافتید.
            feed.load(function (result) {
                if (!result.error) {
                    for (var i = 0; i < result.feed.entries.length; i++) {
                        var entry = result.feed.entries[i];
                        $('#drupaleasy ul').append('<li><a href="' + entry.link + '">' + entry.title + '</a></li>');
                    }
                }
            });
در این جا من خروجی فید را به صورت یک لیست (ul)  تنظیم کردم ، به این صورت که به ازای هر سطر یک li  همراه با تگ a  تولید کرده ام و به ul  مورد نظر append  کردم. 
فایل HTML :
            <div id="drupaleasy" class="feeds">
            <span>DrupalEasy.ir</span>
                <ul>
                </ul>
                <a href="http://drupaleasy.ir">more</a>
            </div>
همچنین خیلی راحت می‌توانید به کمک CSS  استایل‌های دلخواه خودتون رو اعمال کنید.
        .feeds
        {
            float: right;
            background-color: rgba(234, 242, 243, 0.73);
            margin: 5px;
            border-radius: 20px;
            padding: 8px;
            width: 293px;
            height: 217px;
            border: 1px solid #293883;
        }

        #drupaleasy ul
        {
            list-style-image: url("img/drupal.png");
        }
این هم شد خروجی کار  من 

مطالب
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطلب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال اول اکتفاء خواهد شد.


تغییرات مورد نیاز جهت فعال سازی ویرایش، حذف و افزودن رکوردهای jqGrid

می‌خواهیم در بدو نمایش گرید، یک ستون خاص دارای دکمه‌های ویرایش و حذف ظاهر شوند:


برای اینکار تنها کافی است در انتهای ستون‌های تعریف شده، یک ستون خاص را با formatter مساوی actions ایجاد کنیم:
                colModel: [
                    {
                        // سایر ستون‌ها
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
برای اینکه دکمه‌های ویرایش و حذف ردیف‌های آن عمل کنند:


نیاز است تعاریف سایر ستون‌هایی را که باید قابلیت ویرایش داشته باشند، به نحو ذیل تغییر دهیم:
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 70,
                        editable: false
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Supplier.Id', index: 'Supplier.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("SuppliersSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Category.Id', index: 'Category.Id', align: 'right', width: 110,
                        editable: true, edittype: 'select',
                        editoptions: {
                            dataUrl: '@Url.Action("CategoriesSelect","Home")'
                        },
                        editrules: {
                            required: true
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editrules: {
                            required: true,
                            number: true,
                            minValue: 0
                        }
                    },
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false, formatter: 'actions',
                        formatoptions: {
                            keys: true
                        }
                    }
                ],
- در اینجا هر ستونی که دارای خاصیت editable مساوی true است، قابلیت ویرایش پیدا می‌کند.
- edittype آن بیانگر کنترلی است که باید حین ویرایش آن سلول خاص ظاهر شود. برای مثال اگر text باشد، یک text box و اگر مانند حالت Supplier.Id مساوی select تعریف شود، یک drop down را ظاهر خواهد کرد. برای مقدار دهی این drop down می‌توان editoptions و سپس dataUrl آن‌را مقدار دهی نمود.
        public ActionResult SuppliersSelect()
        {
            var list = ProductDataSource.LatestProducts;
            var suppliers = list.Select(x => new SelectListItem
            {
                Text = x.Supplier.CompanyName,
                Value = x.Supplier.Id.ToString(CultureInfo.InvariantCulture)
            }).ToList();
            return PartialView("_SelectPartial", suppliers);
        }
در مثال فوق، این dataUrl به اکشن متد SuppliersSelect اشاره می‌کند که نهایتا لیستی از تولید کننده‌ها را توسط partial view ذیل بازگشت می‌دهد:
 @model IList<SelectListItem>

@Html.DropDownList("srch", Model)
در کل مقادیر قابل تنظیم در اینجا شامل  text، textarea، select، checkbox، password، button، image، file و custom هستند.
- خاصیت editrules، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool، خاصیت‌های minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است (مثال‌های حالت custom را در منابع انتهای بحث می‌توانید مطالعه کنید).
- پس از اینکه مشخص شدند کدامیک از ستون‌ها باید قابلیت ویرایش داشته باشند، مسیری که باید اطلاعات نهایی را به سرور ارسال کند، توسط خاصیت editurl مشخص می‌شود:
$('#list').jqGrid({
   caption: "آزمایش چهارم",
   //url from wich data should be requested
   url: '@Url.Action("GetProducts","Home")',
   //url for edit operation
   editurl: '@Url.Action("EditProduct","Home")',
اکشن متد متناظر با این آدرس یک چنین شکلی را می‌تواند داشته باشد:
        [HttpPost]
        public ActionResult EditProduct(Product postData)
        {
            //todo: Edit product based on postData

            return Json(true);
        }
- تعاریف مسیرهای ارسال اطلاعات Add و Delete، در قسمت تنظیمات navGrid باید ذکر شوند:
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
امضای این اکشن متدها نیز بسیار شبیه به اکشن متد ویرایش است:
        [HttpPost]
        public ActionResult DeleteProduct(string id)
        {
            //todo: Delete product
            return Json(true);
        }

        [HttpPost]
        public ActionResult AddProduct(Product postData)
        {
            //todo: Add product to repository
            return Json(true);
        }
- حالت ویرایش و حذفی که تا اینجا بررسی شد (ستون actions)، جزو خواص توکار این گرید است. اگر بخواهیم آن‌ها را دستی فعال کنیم (جهت اطلاعات عمومی) می‌توان از فراخوانی متد ذیل نیز کمک گرفت:
        var lastSel;
        function inlineEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: false, search: false },
                //edit options
                {},
                //add options
                { width: 'auto', url: '@Url.Action("AddProduct","Home")' },
                //delete options
                { url: '@Url.Action("DeleteProduct","Home")' }
                );
            //add onSelectRow event to support inline edit
            $('#list').setGridParam({
                onSelectRow: function (id) {
                    if (id && id != lastSel) {
                        //save changes in row
                        $('#list').saveRow(lastSel, false);
                        lastSel = id;
                    }
                    //trigger inline edit for row
                    $('#list').editRow(id, true);
                }
            });
        };
در اینجا ابتدا همان تنظیمات مسیرهای Add و Delete انجام شده‌است. سپس با فراخوانی دستی متد editRow در زمان کلیک بر روی یک ردیف، همان کاری را که ستون actions در جهت فعال سازی خودکار حالت ویرایش سلول‌ها انجام می‌دهد، می‌توان شبیه سازی کرد. متد saveRow نیز کار ارسال اطلاعات تغییر کرده را به سرور انجام می‌دهد.
- برای فعال سازی خودکار فرم‌های افزودن رکوردها و یا ویرایش ردیف‌های موجود می‌توان از فراخوانی متد formEdit ذیل کمک گرفت:
        function formEdit() {
            $('input[name=rdEditApproach]').attr('disabled', true);
            $('#list').navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    width: 'auto', checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //add options
                {
                    width: 'auto', url: '@Url.Action("AddProduct","Home")',
                    reloadAfterSubmit: false, checkOnUpdate: true, checkOnSubmit: true,
                    beforeShowForm: function (form) {
                        centerDialog(form, $('#list'));
                    }
                },
                //delete options
                {
                    url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: false
                })
            .jqGrid('navButtonAdd', "#pager", {
                caption: "حذف ردیف‌های انتخابی", title: "Delete Toolbar", buttonicon: 'ui-icon ui-icon-trash',
                onClickButton: function () {
                    var idsList = jQuery("#list").jqGrid('getGridParam', 'selarrrow');
                    alert(idsList);
                    //jQuery("#list").jqGrid('delGridRow',idsList,{reloadAfterSubmit:false});
                }
            });
        };

        function centerDialog(form, grid) {
            var dlgDiv = $("#editmod" + grid[0].id);
            var parentDiv = dlgDiv.parent(); // div#gbox_list
            var dlgWidth = dlgDiv.width();
            var parentWidth = parentDiv.width();
            var dlgHeight = dlgDiv.height();
            var parentHeight = parentDiv.height();
            var parentTop = parentDiv.offset().top;
            var parentLeft = parentDiv.offset().left;
            dlgDiv[0].style.top =  Math.round(  parentTop  + (parentHeight-dlgHeight)/2  ) + "px";
            dlgDiv[0].style.left = Math.round(  parentLeft + (parentWidth-dlgWidth  )/2 )  + "px";
        }
ابتدای تنظیمات آن، شاهد add: true, del: true, edit: true هستید. این مورد سبب می‌شود تا در فوتر گرید، سه دکمه‌ی افزودن، ویرایش و حذف ردیف‌ها ظاهر شوند:


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


و با کلیک بر روی دکمه‌ی ویرایش ردیفی انتخاب شده، صفحه‌ی ویرایش آن ردیف به همراه مقادیر سلول‌های آن ظاهر خواهند شد:


تنظیمات قسمت‌های Add و Delete ویرایش توسط فرم‌ها، با حالت ویرایش داخل ردیفی آنچنان تفاوتی ندارد. فقط در اینجا پیش از نمایش فرم، از متد centerDialog برای نمایش صفحات افزودن و ویرایش رکوردها در وسط صفحه، استفاده شده‌است. توسط checkOnUpdate: true, checkOnSubmit: true سبب خواهیم شد تا اگر کاربر مقادیر موجود فرمی را تغییر داده‌است و سعی در بستن فرم، بدون ذخیره سازی اطلاعات کند، پیغام هشدار دهنده‌ای به او نمایش داده شود که آیا می‌خواهید تغییرات را ذخیره کنید یا خیر؟


- در انتهای متد formEdit، به کمک متد jqGrid و پارامتر navButtonAdd یک دکمه‌ی سفارشی را نیز اضافه کرده‌ایم. اگر به ستون پس از شماره‌های خودکار ردیف‌ها، در سمت راست گرید دقت کنید، یک سری chekbox قابل مشاهده هستند. برای فعال سازی خودکار آن‌ها کافی است خاصیت multiselect گرید به true تنظیم شود. اکنون برای دسترسی به این ستون‌های انتخاب شده، می‌توان از متد jqGrid به همراه پارامترهای getGridParam و selarrrow استفاده کرد. خروجی آن، لیست idهای ستون‌ها است.


برای مطالعه بیشتر

Common Editing Properties
Inline Editing
Form Editing
Cell Editing


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid04.zip
مطالب
Angular Material 6x - قسمت ششم - کار با فرم‌ها و دیالوگ‌ها
در این قسمت قصد داریم به لیست فعلی کاربران و تماس‌های تعریف شده، تماس‌های جدیدی را اضافه کنیم و می‌خواهیم این‌کار را توسط دیالوگ‌های Popup بسته‌ی Angular Material انجام دهیم.


معرفی سرویس MatDialog

توسط سرویس MatDialog می‌توان modal dialogs بسته‌ی Angular Material را نمایش داد که به همراه طراحی متریال و پویانمایی مخصوص آن است.
 let dialogRef = dialog.open(UserProfileComponent,  { height: '400px’,  width: '600px’  });
در اینجا یک صفحه‌ی دیالوگ، توسط متد open آن باز خواهد شد. پارامتر اول آن کامپوننتی است که باید بارگذاری شود و پارامتر دوم آن یک شیء تنظیمات اختیاری است. خروجی این متد وهله‌ای است از MatDialogRef و توسط آن می‌توان به دیالوگ باز شده دسترسی یافت:
dialogRef.afterClosed().subscribe(result => {
   console.log(`Dialog result: ${result}`);
});
dialogRef.close('value');
از آن می‌توان برای بستن dialog و یا دریافت پیامی پس از بسته شدن دیالوگ، استفاده کرد.
در این مثال اگر dialogRef را با متد close و پارامتر value فراخوانی کنیم، سبب بسته شدن این دیالوگ خواهیم شد. این پارامتر در قسمت Dialog result پیام دریافتی پس از بسته شدن دیالوگ نیز قابل دسترسی است.
کامپوننت‌هایی که توسط سرویس MatDialog نمایش داده می‌شوند، می‌توانند توسط سرویس جنریک MatDialogRef، صفحه‌ی دیالوگ باز شده را ببندند:
@Component({/* ... */})
export class YourDialog {

   constructor(public dialogRef: MatDialogRef<YourDialog>) { }

   closeDialog() {
      this.dialogRef.close('Value….!’);
   }
}
نکته‌ی مهم: چون سرویس MatDialog کار وهله سازی و نمایش کامپوننت‌ها را به صورت پویا انجام می‌دهد، محل تعریف این نوع کامپوننت‌های ویژه در قسمت entryComponents ماژول مرتبط است. به این ترتیب به A head of time compiler اعلام می‌کنیم که component factory این کامپوننت پویا است و باید به نحو ویژه‌ای مدیریت شود.

نحوه‌ی طراحی یک دیالوگ نیز به کمک تعدادی کامپوننت و دایرکتیو میسر است:
<h2 mat-dialog-title>Delete all</h2>
<mat-dialog-content>Are you sure?</mat-dialog-content>
<mat-dialog-actions>
    <button mat-button mat-dialog-close>No</button>
    <!-- Can optionally provide a result for the closing dialog. -->
    <button mat-button [mat-dialog-close]="true">Yes</button>
</mat-dialog-actions>
دایرکتیو mat-dialog-title سبب نمایش عنوان دیالوگ می‌شود. mat-dialog-content محتوای قابل اسکرول این دیالوگ را در بر می‌گیرد. mat-dialog-actions محلی است برای قرارگیری action buttons در پایین صفحه‌ی دیالوگ. در اینجا اگر ویژگی mat-dialog-close به true تنظیم شود، آن دکمه قابلیت بستن دیالوگ را پیدا می‌کند.


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

قبل از هر کاری نیاز است دکمه‌ی افزودن یک کاربر جدید را به صفحه اضافه کنیم. برای اینکار یک منوی ویژه را در سمت راست، بالای صفحه ایجاد می‌کنیم. بنابراین ابتدا به مستندات toolbar و menu مراجعه می‌کنیم تا با نحوه‌ی تعریف دکمه‌ها و منوها به toolbar آشنا شویم. سپس فایل قالب toolbar\toolbar.component.html را به صورت زیر تکمیل می‌کنیم:
<mat-toolbar color="primary">
  <button mat-button fxHide fxHide.xs="false" (click)="toggleSidenav.emit()">
    <mat-icon>menu</mat-icon>
  </button>

  <span>Contact Manager</span>

  <span fxFlex="1 1 auto"></span>
  <button mat-button [matMenuTriggerFor]="menu">
    <mat-icon>more_vert</mat-icon>
  </button>
  <mat-menu #menu="matMenu">
    <button mat-menu-item>New Contact</button>
  </mat-menu>
</mat-toolbar>
توسط یک span و سپس fxFlex تعریف شده‌ی آن سبب خواهیم شد تا mat-button بعدی و محتوای پس از آن به گوشه‌ی سمت راست toolbar هدایت شوند؛ در غیراینصورت دقیقا در کنار عبارت Contact manager ظاهر خواهند شد.
سپس ابتدا یک mat-button را با آیکن more_vert (آیکن علامت بیشتر عمودی) تعریف کرده‌ایم:


این دکمه توسط ویژگی matMenuTriggerFor به template reference variable ایی به نام menu متصل شده‌است تا با کلیک بر روی آن، این mat-menu را نمایش دهد:



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

پس از تعریف دکمه و منویی که سبب نمایش عبارت افزودن یک تماس جدید می‌شوند، به رخ‌داد کلیک آن متدی را جهت نمایش صفحه‌ی دیالوگ جدید اضافه می‌کنیم:
 <button mat-menu-item (click)="openAddContactDialog()">New Contact</button>
سپس در کدهای کامپوننت toolbar، کار مدیریت آن‌را انجام خواهیم داد. اما پیش از آن بهتر است کامپوننت جدیدی را که قرار است نمایش دهد به برنامه اضافه کنیم:
 ng g c contact-manager/components/new-contact-dialog --no-spec
این دستور علاوه بر تولید کامپوننت جدید new-contact-dialog در پوشه‌ی components، کار تعریف مدخل آن‌را در ماژول این قسمت نیز انجام می‌دهد. اما همانطور که پیشتر نیز عنوان شد، باید آن‌را به لیست entryComponents اضافه کنیم تا بتوان آن‌را به صورت پویا بارگذاری کرد (در غیر اینصورت در زمان نمایش پویای آن، خطای no component factory for را دریافت می‌کنیم). بنابراین فایل contact-manager.module.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { NewContactDialogComponent } from "./components/new-contact-dialog/new-contact-dialog.component";

@NgModule({
  declarations: [
    NewContactDialogComponent],
  entryComponents: [
    NewContactDialogComponent
  ]
})
export class ContactManagerModule { }
اکنون می‌توانیم سرویس MatDialog را به سازنده‌ی کامپوننت toolbar تزریق کرده و از آن برای نمایش این کامپوننت جدید استفاده کنیم:
import { Component, EventEmitter, OnInit, Output } from "@angular/core";
import { MatDialog } from "@angular/material";

import { NewContactDialogComponent } from "../new-contact-dialog/new-contact-dialog.component";

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

  @Output() toggleSidenav = new EventEmitter<void>();

  constructor(private dialog: MatDialog) { }

  ngOnInit() { }

  openAddContactDialog(): void {
    const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" });
    dialogRef.afterClosed().subscribe(result => {
      console.log("The dialog was closed", result);
    });
  }
}
در اینجا سرویس MatDialog به سازنده‌ی کامپوننت تزریق شده و سپس از آن در متد openAddContactDialog که متصل به منوی نمایش «new contact» است، جهت نمایش پویای کامپوننت NewContactDialogComponent، استفاده می‌شود. اکنون اگر برنامه را اجرا کنیم و گزینه‌ی «new contact» را از منو انتخاب کنیم، چنین تصویری حاصل خواهد شد:



تکمیل قالب کامپوننت تماس جدید

در ادامه می‌خواهیم فرم افزودن یک تماس جدید را به همراه فیلدهای ورودی آن، به قالب new-contact-dialog.component.html اضافه کنیم:
<h2 mat-dialog-title>Add new contact</h2>
<mat-dialog-content>
  <div fxLayout="column">

  </div>
</mat-dialog-content>
<mat-dialog-actions>
  <button mat-button color="primary" (click)="save()">
    <mat-icon>save</mat-icon> Save
  </button>
  <button mat-button color="primary" (click)="dismiss()">
    <mat-icon>cancel</mat-icon> Cancel
  </button>
</mat-dialog-actions>
تا اینجا ساختار مقدماتی صفحه دیالوگ را مطابق توضیحاتی که در ابتدای بحث عنوان شد، تکمیل کردیم. یک عنوان توسط mat-dialog-title به آن اضافه شده‌است. سپس mat-dialog-content اضافه شده که در ادامه آن‌را تکمیل می‌کنیم. در آخر هم دکمه‌های action به این دیالوگ استاندارد اضافه شده‌اند. برای تکمیل متدهای save و dismiss این دکمه‌ها، کدهای ذیل را به کامپوننت new-contact-dialog.component.ts اضافه می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { MatDialogRef } from "@angular/material";

@Component()
export class NewContactDialogComponent implements OnInit {

  constructor(
    private dialogRef: MatDialogRef<NewContactDialogComponent>
  ) { }

  ngOnInit() {
  }

  save() {
  }

  dismiss() {
    this.dialogRef.close(null);
  }
}
در اینجا توسط سرویس MatDialogRef از نوع NewContactDialogComponent، می‌توانیم به ارجاعی از سرویس MatDialog باز کننده‌ی آن دسترسی پیدا کنیم و برای نمونه در متد dismiss سبب بسته شدن این دیالوگ شویم.
تا اینجا اگر برنامه را اجرا کنیم، به چنین شکلی خواهیم رسید:



تکمیل فیلدهای ورود اطلاعات فرم ثبت یک تماس جدید

تا اینجا ساختار فرم دیالوگ ثبت اطلاعات جدید را تکمیل کردیم. این فرم، به شیء user متصل خواهد شد. همچنین لیستی از avatars را هم جهت انتخاب، نمایش می‌دهد. به همین جهت این دو خاصیت عمومی را به کدهای کامپوننت آن اضافه می‌کنیم:
import { User } from "../../models/user";

@Component()
export class NewContactDialogComponent implements OnInit {

  avatars = ["user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8"];
  user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null };
در ادامه فیلدهای آن‌را به صورت زیر در قسمت mat-dialog-content اضافه خواهیم کرد:


الف) فیلد نمایش و انتخاب avatar کاربر
    <mat-form-field>
      <mat-select placeholder="Avatar" [(ngModel)]="user.avatar">
        <mat-select-trigger>
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.avatar }}
        </mat-select-trigger>
        <mat-option *ngFor="let avatar of avatars" [value]="avatar">
          <mat-icon svgIcon="{{avatar}}"></mat-icon> {{ avatar }}
        </mat-option>
      </mat-select>
    </mat-form-field>


در اینجا از کامپوننت mat-select برای انتخاب avatar کاربر استفاده شده‌است که نتیجه‌ی نهایی انتخاب آن به خاصیت user.avatar متصل شده‌است.
گزینه‌های این لیست (mat-option) بر اساس آرایه‌ی avatars که در کامپوننت تعریف کردیم، تامین می‌شوند که در اینجا از mat-icon برای نمایش آیکن مرتبط نیز استفاده شده‌است. در این مورد در قسمت قبل چهارم، بخش «بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material» بیشتر توضیح داده شده‌است.
کار mat-select-trigger، سفارشی سازی برچسب نمایشی این کنترل است.

ب) فیلد دریافت نام کاربر به همراه اعتبارسنجی آن
    <mat-form-field>
      <input matInput placeholder="Name" #name="ngModel" [(ngModel)]="user.name" required>
      <mat-error *ngIf="name.invalid && name.touched">You must enter a name</mat-error>
    </mat-form-field>


در اینجا فیلد نام کاربر، به user.name متصل و همچنین توسط ویژگی required، پر کردن آن الزامی اعلام شده‌است. به همین جهت در تصویر فوق یک ستاره را نیز کنار آن مشاهده می‌کند که به صورت خودکار توسط Angular Material نمایش داده شده‌است.
از کامپوننت mat-error برای نمایش خطاهای اعتبارسنجی یک فیلد استفاده می‌شود که نمونه‌ای از آن‌را در اینجا با بررسی خواص invalid  و touched فیلد نام که بر اساس ویژگی required فعال می‌شوند، مشاهده می‌کنید. بدیهی است در اینجا به هر تعدادی که نیاز است می‌توان mat-error را قرار داد.

ج) فیلد دریافت تاریخ تولد کاربر توسط یک date picker
    <mat-form-field>
      <input matInput [matDatepicker]="picker" placeholder="Born" [(ngModel)]="user.birthDate">
      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
      <mat-datepicker #picker></mat-datepicker>
    </mat-form-field>


در اینجا از کامپوننت mat-datepicker برای انتخاب تاریخ تولید یک شخص استفاده شده‌است و نتیجه‌ی آن به خاصیت user.birthDate متصل خواهد شد.
برای افزودن آن ابتدا یک mat-datepicker را به mat-form-field اضافه می‌کنیم. سپس یک template reference variable را به آن نسبت خواهیم داد. از آن هم در فیلد ورودی با انتساب آن به ویژگی matDatepicker و هم در کامپوننت mat-datepicker-toggle که سبب نمایش آیکن انتخاب تقویم می‌شود، در ویژگی for آن استفاده خواهیم کرد.

د) فیلد چند سطری دریافت توضیحات و شرح‌حال کاربر
    <mat-form-field>
      <textarea matInput placeholder="Bio" [(ngModel)]="user.bio"></textarea>
    </mat-form-field>


در اینجا برای دریافت توضیحات چندسطری، از یک text area استفاده شده‌است که به خاصیت user.bio متصل است.

بنابراین همانطور که ملاحظه می‌کنید، روش طراحی فرم‌های Angular Material ویژگی‌های خاص خودش را دارد:
- دایرکتیو matInput را می‌توان به المان‌های استاندارد input و textarea اضافه کرد تا داخل mat-form-field نمایش داده شوند. این mat-form-field است که کار اعمال CSS ویژه‌ی طراحی متریال را انجام می‌دهد و امکان نمایش پیام‌های خطای اعتبارسنجی و پویانمایی ورود اطلاعات را سبب می‌شود.
- قسمت mat-dialog-content را توسط fxLayout به حالت ستونی تنظیم کردیم:
<mat-dialog-content>
  <div fxLayout="column">

  </div>
</mat-dialog-content>
این مورد است که سبب خواهد شد المان‌های فرم از بالا به پایین نمایش داده شوند. در غیراینصورت mat-form-fieldها دقیقا در کنار هم قرار می‌گیرند.
برای مثال اگر خواستید المان‌های فرم با فاصله‌ی بیشتری از هم قرار بگیرند، می‌توان از fxLayoutGap استفاده کرد که در مورد آن در قسمت دوم «معرفی Angular Flex layout» بیشتر توضیح داده شد.


تکمیل سرویس کاربران جهت ذخیره‌ی اطلاعات تماس کاربر جدید

در ادامه می‌خواهیم با کلیک کاربر بر روی دکمه‌ی Save، ابتدا این اطلاعات به سمت سرور ارسال و سپس در سمت سرور ذخیره شوند. پس از آن، Id این کاربر جدید به سمت کلاینت بازگشت داده شود، دیالوگ جاری بسته و در آخر این شیء جدید به لیست تماس‌های نمایش داده‌ی شده‌ی در sidenav اضافه گردد.

الف) تکمیل سرویس Web API سمت سرور
در ابتدا متد Post را به Web API برنامه جهت ذخیره سازی اطلاعات User ارسالی از سمت کلاینت اضافه می‌کنیم. کدهای کامل آن‌را از فایل پیوستی انتهای بحث می‌توانید دریافت کنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpPost]
        public async Task<IActionResult> Post([FromBody] User user)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
            await _usersService.AddUserAsync(user);
            return Created("", user);
        }
    }
}

ب) تکمیل سرویس کاربران سمت کلاینت
سپس به فایل user.service.ts مراجعه کرده و دو تغییر زیر را به آن اضافه می‌کنیم:
@Injectable({
  providedIn: "root"
})
export class UserService {

  private usersSource = new BehaviorSubject<User>(null);
  usersSourceChanges$ = this.usersSource.asObservable();

  constructor(private http: HttpClient) { }

  addUser(user: User): Observable<User> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post<User>("/api/users", user, { headers: headers }).pipe(
        map(response => {
          const addedUser = response || {} as User;
          this.notifyUsersSourceHasChanged(addedUser);
          return addedUser;
        }),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  notifyUsersSourceHasChanged(user: User) {
    this.usersSource.next(user);
  }
}
کار متد addUser، ارسال اطلاعات فرم ثبت یک تماس جدید به سمت سرور و Web API برنامه است. پس از ثبت موفقیت آمیز کاربر در سمت سرور، متد return Created آن:
 return Created("", user);
سبب خواهد شد تا بتوانیم در سمت کلاینت، به Id اطلاعات رکورد جدید دسترسی داشته باشیم. مزیت آن امکان افزودن این رکورد به لیست کاربران sidenav و همچنین فعالسازی مسیریابی آن است که بر اساس این Id واقعی کار می‌کند.
بنابراین نیاز است از طریق این سرویس به کامپوننت sidenav، در مورد تغییرات لیست کاربران اطلاعات رسانی کنیم که روش کار آن‌را پیشتر در مطلب «صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular» نیز مرور کرده‌ایم. برای این منظور یک BehaviorSubject از نوع User را تعریف کرده‌ایم که اشتراک به آن از طریق خاصیت عمومی usersSourceChanges میسر است. هر زمانیکه متد next آن فراخوانی شود، تمام مشترکین به آن، از افزوده شدن کاربر جدید، به همراه اطلاعات کامل آن مطلع خواهند شد.

ج) تکمیل متد save کامپوننت new-contact-dialog
پس از تکمیل سرویس کاربران جهت افزودن متد addUser به آن، اکنون می‌توانیم از آن در کامپوننت دیالوگ افزودن اطلاعات تماس جدید استفاده کنیم:
import { UserService } from "../../services/user.service";

@Component()
export class NewContactDialogComponent {

  user: User = { id: 0, birthDate: new Date(), name: "", avatar: "", bio: "", userNotes: null };

  constructor(
    private dialogRef: MatDialogRef<NewContactDialogComponent>,
    private userService: UserService
  ) { }

  save() {
    this.userService.addUser(this.user).subscribe(data => {
      console.log("Saved user", data);
      this.dialogRef.close(data);
    });
  }
}
در اینجا در متد save، ابتدا متد addUser سرویس افزودن اطلاعات جدید فراخوانی می‌شود. سپس در صورت موفقیت آمیز بودن عملیات، توسط سرویس dialogRef، این صفحه‌ی دیالوگ نیز به صورت خودکار بسته خواهد شد. همچنین به متد close آن data دریافتی از سرور ارسال شده‌است. این data در toolbar.component در قسمت dialogRef.afterClosed قابل دسترسی خواهد بود.

د) تکمیل کامپوننت sidenav جهت واکنش نشان دادن به افزوده شدن اطلاعات تماس جدید
اکنون که سرویس کاربران به صفحه دیالوگ افزودن اطلاعات یک تماس جدید متصل شده‌است، نیاز است بتوانیم اطلاعات کاربر جدید را به لیست تماس‌های sidenav اضافه کنیم. به همین جهت به sidenav.component مراجعه کرده و مشترک usersSourceChanges سرویس کاربران خواهیم شد:
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  subscription: Subscription | null = null;

  constructor(
    private userService: UserService) { }

  ngOnInit() {
    this.subscription = this.userService.usersSourceChanges$.subscribe(user => {
      if (user) {
        this.users.push(user);
      }
    });
  }

  ngOnDestroy() {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
  }
}
ابتدا در ngOnInit توسط سرویس کاربران، مشترک تغییرات usersSourceChanges خواهیم شد. در اینجا اگر کاربر جدیدی به لیست اضافه شده باشد، آن‌را توسط متد push به لیست کاربران جاری sidenav اضافه می‌کنیم تا بلافاصله در لیست نمایش داده شود.


استفاده از کامپوننت Snackbar جهت نمایش موفقیت آمیز بودن ثبت اطلاعات

متد save کامپوننت دیالوگ یک تماس جدید را به صورت زیر تکمیل کردیم:
  save() {
    this.userService.addUser(this.user).subscribe(data => {
      console.log("Saved user", data);
      this.dialogRef.close(data);
    });
در اینجا data ارسال شده‌ی به متد close در کامپوننت toolbar در قسمت dialogRef.afterClosed قابل دسترسی خواهد بود:
  openAddContactDialog(): void {
    const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" });
    dialogRef.afterClosed().subscribe(result => {
      console.log("The dialog was closed", result);
    });
  }
بنابراین در ادامه قصد داریم از آن جهت نمایش یک snackbar به همراه ارائه لینک هدایت به صفحه‌ی جزئیات تماس جدید، استفاده کنیم:


کدهای کامل این تغییرات را در ذیل مشاهده می‌کنید:
@Component()
export class ToolbarComponent {

  @Output() toggleSidenav = new EventEmitter<void>();

  constructor(private dialog: MatDialog, private snackBar: MatSnackBar,  private router: Router) { }

  openAddContactDialog(): void {
    const dialogRef = this.dialog.open(NewContactDialogComponent, { width: "450px" });
    dialogRef.afterClosed().subscribe((result: User) => {
      console.log("The dialog was closed", result);
      if (result) {
        this.openSnackBar(`${result.name} contact has been added.`, "Navigate").onAction().subscribe(() => {
          this.router.navigate(["/contactmanager", result.id]);
        });
      }
    });
  }

  openSnackBar(message: string, action: string): MatSnackBarRef<SimpleSnackBar> {
    return this.snackBar.open(message, action, {
      duration: 5000,
    });
  }
}
توضیحات:
برای گشودن snackbar که نمونه‌ای از آن‌را در تصویر فوق ملاحظه می‌کنید، ابتدا نیاز است سرویس MatSnackBar را به سازنده‌ی کلاس تزریق کرد. سپس توسط آن می‌توان یک کامپوننت مستقل را همانند دیالوگ‌ها نمایش داد و یا می‌توان یک متن را به همراه یک Action منتسب به آن، به کاربر نمایش داد؛ مانند متد openSnackBar که در کامپوننت فوق از آن استفاده می‌شود. این متد در رخ‌داد پس از بسته شدن dialog، نمایش داده شده‌است.
پارامتر اول آن پیامی است که توسط snackbar نمایش داده می‌شود و پارامتر دوم آن، برچسب دکمه مانندی است کنار این پیام، که سبب انجام عملی خواهد شد و در اینجا به آن Action گفته می‌شود. برای مدیریت آن باید متد onAction را فراخوانی کرد و مشترک آن شد. در این حالت اگر کاربر بر روی این دکمه‌ی action کلیک کند، سبب هدایت خودکار او به صفحه‌ی نمایش جزئیات اطلاعات تماس کاربر خواهیم شد. به همین جهت سرویس Router نیز به سازنده‌ی کلاس تزریق شده‌است تا بتوان از متد navigate آن استفاده کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-05.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
بازخوردهای دوره
استفاده از Async و Await در برنامه‌های ASP.NET MVC
- پیشنیاز مطالعه قسمت جاری، مطالعه 6 قسمت اول این دوره است.
- «همزمان اجرا میشه»
خیر. متدهای Async واقعی مثل نمونه ارائه شده در EF غیرهمزمان اجرا می‌شوند. یعنی، ترد جاری را آزاد کرده و ASP.NET می‌تواند از آن ترد برای پاسخ دهی به یک درخواست رسیده دیگر استفاده کند.
- «باید منتظر پاسخ از db بمونه»
استفاده از await و async سبب بازنویسی بدنه متد توسط یک state machine در پشت صحنه می‌شوند. یعنی اینطور نیست که روش اجرای آن blocking است و تا رسیدن پاسخ از بانک اطلاعاتی، از این ترد دیگر نمی‌شود استفاده کرد. جایی که await فراخوانی می‌شود، ترد جاری برای استفاده بعدی آزاد خواهد شد. در ادامه مابقی کدها تبدیل به یک IEnumerator می‌شوند که هر دستور آن شامل یک yield return است. هر مرحله که تمام شد، MoveNext این IEnumerator فراخوانی می‌شود تا به مرحله‌ی بعدی برسد. به این روش استفاده از coroutines هم گفته می‌شود که در سی شارپ 5، کامپایلر کار تولید کدهای آن‌را انجام می‌دهد. برای مطالعه بیشتر:
انجام پی در پی اعمال Async به کمک Iterators - قسمت اول  
انجام پی در پی اعمال Async به کمک Iterators - قسمت دوم
- «چون تا فایل آپلود نشه ذخیره آدرس تو db بی معنیه»
ذخیره آدرس هم یک قسمت از کار است و اتفاقا وابسته به سیستم جاری هم نیست. وابسته است به یک بانک اطلاعاتی که خارج از مرزهای سیستم، به صورت مستقل در حال فعالیت است (عموما البته؛ مثلا اگر از SQL Server استفاده می‌شود).
برای ذخیره فایل‌ها در سیستم هم متدهای Async به کلاس Stream در دات نت 4.5 اضافه شده‌اند؛ مثل WriteAsync . در این حالت هم می‌توان از await WriteAsync برای ذخیره اطلاعات و بازهم آزاد کردن ترد جاری استفاده کرد.
نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
یک نکته‌ی تکمیلی: روش تعیین حداکثر اندازه‌ی فایل قابل آپلود در برنامه‌های ASP.NET Core
الف) اگر برنامه توسط IIS هاست شده‌است
<?xml version="1.0" encoding="utf-8"?>
<configuration>
 <!-- To customize the asp.net core module uncomment and edit the following section. 
 For more info see https://go.microsoft.com/fwlink/?linkid=838655 -->
 <system.webServer>
  <handlers>
  <remove name="aspNetCore"/>
  <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
  </handlers>
  <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
  <security>
   <requestFiltering>
    <!-- This will handle requests up to 50MB -->
    <requestLimits maxAllowedContentLength="52428800" />
   </requestFiltering>
  </security>
  </system.webServer>
</configuration>
در اینجا (در فایل web.config برنامه) مقدار خاصیت maxAllowedContentLength است که تعیین کننده‌ی حداکثر اندازه‌ی فایل قابل آپلود است. به علاوه مطمئن شوید که فایل web.config شما حتما publish شده.
همچنین requestTimeout را نیز به صورت زیر می‌توان مقدار دهی کرد:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <aspNetCore requestTimeout="00:20:00"  .... />
  </system.webServer>
</configuration>
به همراه این تنظیمات:
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<IISServerOptions>(options =>
            {
                options.MaxRequestBodySize = int.MaxValue;
            });
            services.Configure<FormOptions>(options =>
            {
                options.ValueLengthLimit = int.MaxValue;
                options.MultipartBodyLengthLimit = long.MaxValue; // <-- ! long.MaxValue
                options.MultipartBoundaryLengthLimit = int.MaxValue;
                options.MultipartHeadersCountLimit = int.MaxValue;
                options.MultipartHeadersLengthLimit = int.MaxValue;
            });
 
ب) اگر برنامه در لینوکس و بر اساس وب سرور Kestrel هاست شده‌است
روش اول: استفاده از ویژگی RequestSizeLimit که از نگارش ASP.NET Core 2.0 به بعد قابل استفاده‌است و صرفا به قسمتی از برنامه اعمال می‌شود:
[HttpPost]
[RequestSizeLimit(40000000)]
public async Task<IActionResult> UploadFiles(IFormFile file)
روش دوم: تنظیم سراسری آن برای کل برنامه:
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder
                        .UseStartup<Startup>()
                        .ConfigureKestrel(kestrelServerOptions =>
                        {
                            kestrelServerOptions.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10);
                            kestrelServerOptions.Limits.MaxRequestBodySize = 52428800; //50MB
                        });
                });
بدیهی است تنظیم این دو روش در حالت استفاده‌ی از IIS تاثیری نخواهند داشت.
مطالب
نوشتن یک بات تلگرامی با استفاده از webhookها
با رشد روز افزون شبکه‌های اجتماعی و نیاز روزمره مردم به این شبکه‌ها ،اکثر شبکه‌های اجتماعی با در اختیار قرار دادن کتاب خانه‌ها و apiها، توسعه و طراحی یک برنامه‌ی مبتنی بر آن‌ها را فراهم کرده‌اند. تلگرام نیز یکی  از این شبکه‌ها است و با طراحی بات‌ها میتوان یک نرم افزار کوچک و پر کاربرد را جهت آن طراحی کرد.
در این مقاله قصد دارم نحوه ساخت یک بات تلگرامی را با استفاده از webhook که پیشنهاد خود تلگرام میباشد و همچنین کار با سایر apiهای مانند گرفتن عکس پروفایل و ... به اشتراک بگذارم. ما آموزش را بنا بر یک مثال کاربردی، در قالب یک بات تلگرامی قرار می‌دهیم که بعد از start شدن، پیغام خوش آمد گویی را نمایش میدهد و سپس جملات فارسی را از کاربر دریافت و معادل انگلیسی آن‌ها را با استفاده از از google translate به کاربر نشان میدهد.


شروع به ساخت بات

 به طور کلی دو روش برای ساخت یک بات تلگرامی وجود دارد:
1- استفاده از کتابخانه‌های آماده
2 - استفاده webhook

در روش 1، از یک سری از کتابخانه‌های آماده و تعریف شده، استفاده میکنیم.

مزایا:
 بدون زحمت زیادی و فقط با فراخوانی توابع آماده، قادر خواهیم بود یک بات خیلی ساده را شبیه سازی کنیم.
هزینه آن نسبت به webhook کمتر است و شما میتوانید با یک vps، بات را اجرا کنید.

معایب:
1- این روش ازpolling استفاد میکند. یعنی دستور دریافت شده در یک حلقه‌ی بی نهایت قرا میگیرد و هر بار چک میشود که آیا درخواستی رسیده است یا خیر؟ که سربار بالایی را بر روی سرور ما خواهد داشت.
2- بعد از مدتی down میشود.
3- اگر شمار درخواست‌ها بالا رود، Down میشود.

روش  دیگر استفاده از webhook است که اصولی‌ترین روش و روشی است که خود سایت تلگرام آن را پیشنهاد داده‌است. اگر بخواهم توضیح کوتاهی درباره webhook بدهم، با استفاده از آن میتوانید تعیین کنید وقتی یک event، رخ‌داد، api ایی فرخوانی شود؛ یا مثلا شما یک سایت را با api نوشته‌اید (ASP.NET Web API) و آن را پابلیش کرده‌اید و الان میخواهید یک api جدید را بنویسید. در این حالت با استفاده از webhook، دیگر نیازی نیست تا کل پروژه را پابلیش کنید. یک پروژه api را می‌نویسید و آن را آپلود می‌کنید و درقسمت تنظیم وب هوک، آدرس دامین خودتون را می‌دهید. حتی میتوانید آن را با php  یا هر زبانی که میتوانید بنویسید.

 معایب:
1- هزینه آن. شما علاوه بر تهیه‌ی هاست و دامین، باید ssl را هم فعال کنید که در ادامه بیشتر توضیح خواهیم داد. البته نگرانی برای پیاده سازی ssl نیست. چون سایت‌هایی هستند که این سرویس‌ها را به صورت رایگان در اختیار شما میگذارند (مانند Lets encrypt).
2- تنظیم آن به مراتب سخت‌تر از روش قبل است.

مزایا:
1- سرعت آن بیشتر است.
2- درخواست‌های با تعداد بالا را می‌توان به راحتی پاسخ داد.
3- وابستگی ثالثی ندارد.


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

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


ایجاد پروژه‌ی جدید بات

- در ادامه سراغ ویژوال استودیو رفته و یک پروژه‌ی Web api Empty را ایجاد کنید.
- سپس وارد سایت تلگرام شوید و کتابخانه‌ی مربوطه را دریافت کنیدو یا میتوانید با استفاده از دستور زیر، این کتابخانه را نصب کنید:
 Install-Package Telegram.Bot
پس از آن، اولین کار، ایجاد یک controller جدید به نام Webhook میباشد. درون این کنترلر، یکaction متد جدید را به نام UpdateMsg ایجاد می‌کنیم:
[HttpPost]
public async Task<IHttpActionResult> UpdateMsg(Update update)
{
  //......
}
تمام درخواست‌های تلگرام (وقایع رسیده‌ی از آن) ،به این action متد ارسال خواهند شد. اگر دقت کنید این متد دارای یک آرگومان از نوع update میباشد که شامل تمام پراپرتی‌های یک درخواست، اعم از نام کاربری، نوع درخواست، پیام و ... است.


تنظیم کردن WebHook

- حال به قسمت تنظیم کردن webhook می‌رسیم. وارد فایل Global.asax.cs برنامه شوید و با دستور زیر، وب هوک را تنظیم کنید:
var bot = new Telegram.Bot.TelegramBotClient("Token");
bot.SetWebhookAsync("https://Domian/api/webhook").Wait();
- در قسمت token ،token خود را که از BotFather دریافت کردید، وارد کنید و در قسمت setwebhook، باید ادرس دامنه‌ی خود را وارد نمائید. البته برای آزمایش برنامه، ما دامنه‌ای نداریم و  قصد خرید هاستی را هم نداریم. بنابراین با استفاده از ابزار ngrok می‌توان به سادگی یک دامنه‌ی آزمایشی SSL را تهیه کرد و از آن استفاده نمود.
- در این حالت بعد از اجرای ngrok، آدرس https آن را کپی کرده و در قسمت بالا، بجای Domain ذکر شده قرار دهید.
- برای آزمایش انجام کار، یک break-point را در قسمت action متد یاد شده قرار دهید و سپس برنامه را اجرا کنید.
- اکنون از طریق تلگرام وارد بات شوید و یک درخواست را ارسال کنید.
- اگر کار را به درستی انجام داده باشید، در صفحه ngrok  پیغام 200 مبتنی بر ارسال صحیح درخواست را دریافت خواهید کرد و همچنین در قسمت breakpoints برنامه بر روی آرگومان update، میتوانید پراپرتی‌های یک درخواست را به صورت کامل دریافت کنید.
- ارسال اولین درخواست ما از طریق بات start/ میباشد. در این حالت میتوان دریافت که کاربر برای بار اول است که از بات استفاده میکند.
- در اکشن متد از طریق خاصیت update.Message.Text میتوان به متن فرستاده شده دسترسی داشت.
- همچین اطلاعات کاربر در update.Message.From، همراه با درخواست، فرستاده می‌شود.


کار با ابزار ترجمه‌ی گوگل و تکمیل پروژه‌ی Web API

اکنون طبق مثال بالا می‌خواهیم وقتی کاربر برای اولین بار وارد شد، پیغام خوش آمد گویی به او نمایش داده شود. بعد از آن هر متنی را که فرستاد، معنای آن را از گوگل ترنسلیت گرفته و مجددا به کاربر ارسال میکنیم. برای اینکار کلاس WebhookController را به شکل زیر تکمیل خواهیم کرد: 
namespace Telegrambot.Controllers
{
    public class WebhookController : ApiController
    {
        Telegram.Bot.TelegramBotClient _bot = new Telegram.Bot.TelegramBotClient("number");
        Translator _translator = new Translator();

        [HttpPost]
        public async Task<IHttpActionResult> UpdateMsg(Update update)
        {
            if (update.Message.Text == "/start")
            {
                await _bot.SendTextMessageAsync(update.Message.From.Id, "Welcome To My Bot");
            }
            else
            {
                var translatedRequest = _translator.Translate(update.Message.Text, "Persian", "English");
                await _bot.SendTextMessageAsync(update.Message.From.Id, translatedRequest);
            }
            return Ok(update);
        }
    }
}
توضیحات:
- با استفاده از update.Message.From.Id میتوان پیغام را به شخصی که درخواست داده‌است فرستاد.
- دقت کنید هنگام ارسال درخواست، در ngrok آیا درخواستی فرستاده می‌شود یا خطایی وجود دارد.

نکته! برای استفاده از بات باید حتما از ssl استفاده کنید. اگر نیاز به خرید این سرویس را ندارید، از این لینک نیز می‌توانید سرویس مورد نظر را بعد از 24 ساعت بر روی دامین خود تنظیم کنید.


توضیحات بیشتر در این مورد را مثلا دکمه‌های پویا و گرفتن عکس پروفایل و ....، در مقاله‌ی بعدی قرار خواهم داد.

شما میتوانید از این لینک پروژه بالا را دریافت و اجرا کنید .
نظرات مطالب
آموزش Backload (آپلود چندین فایل به طور همزمان با آجاکس )
نکته ای بسیار مهم در رابطه با کار با Backload:

حذف و اضافه کردن تصاویر با Backload روی لوکال بدون هیچ مشکلی انجام می‌شود اما بعد از اینکه شما پروژه را آپلود کردید خواهید دید که تصاویر اضافه می‌شوند اما  حذف نمی‌شوند.
برای حل این مشکل کد زیر را در تگ <system.webServer>  در فایل web.config قرار دهید :

 <modules runAllManagedModulesForAllRequests="true">
      <remove name="WebDAVModule"/>
  </modules>

موفق باشید .
مطالب
آشنایی با WPF قسمت دوم: Layouts بخش اول
layout‌‌‌ها یکی از مهمترین قسمت‌های یک برنامه‌ی کاربردی هستند. چیدمان کنترل‌ها روی یک ناحیه با دادن مختصات پیکسلی ثابت، ممکن است در یک محیط محدود خود را خوب نشان بدهد ولی به زودی با تغییر محیط برنامه و یا تغییر وضوح تصویر صفحه نمایش، برنامه از کنترل خارج خواهد شد؛ در نتیجه استفاده از Layout‌ها یا پنل‌ها در WPF امری حیاتی و مهم هستند. layout‌‌ها که با نام container هم شناخته می‌شوند وظیفه دارند که بگویند چه کنترل‌هایی در کجا و چگونه باید در صفحه‌ی برنامه قرار بگیرند. پنل‌های توکار در WPF به دسته‌های زیر تقسیم می‌شوند:
  • Grid Panel
  • Stack Panel
  • Dock Panel
  • Wrap Panel
  • Canvas Panel

StackPanel

این پنل یکی از ساده‌ترین و سودمندترین پنل هاست که بر اساس جهت Orientation افقی یا عمودی که به آن تنظیم می‌شود، کنترل‌ها را کنار یکدیگر یا زیر یکدیگر قرار می‌دهد. این کنترل برای ایجاد و تهیه لیست‌های مختلف مناسب است. در ساختار داخلی کنترل‌های لیست و کامبو و منو‌ها که در WPF موجود هستند این پنل استفاده شده است. کد زیر یک نمونه کاربرد Stack Panel را نشان می‌دهد که به صورت عمودی چینش شده است.
<StackPanel>
  <TextBlock Margin="10" FontSize="20">How do you like your coffee?</TextBlock>
  <Button Margin="10">Black</Button>
  <Button Margin="10">With milk</Button>
  <Button Margin="10">Latte machiato</Button>
  <Button Margin="10">Chappuchino</Button>
</StackPanel>

نکته‌ی مهم اینکه میتوانید در اینجا از یک nested layout هم استفاده کنید بدین صورت که یک layout را داخل یک layout دیگر قرار دهید. کد زیر ترکیب دو stack panel را به صورت افقی و عمودی به ما نشان می‌دهد:

<StackPanel Orientation="Vertical"> <!-- Vertical is the default -->
  <Label Background="Red">Red 1</Label>
  <Label Background="LightGreen">Green 1</Label>
  <StackPanel Orientation="Horizontal">
    <Label Background="Red">Red 2</Label>
    <Label Background="LightGreen">Green 2</Label>
    <Label Background="LightBlue">Blue 2</Label>
    <Label Background="Yellow">Yellow 2</Label>
    <Label Background="Orange">Orange 2</Label>
  </StackPanel>
  <Label Background="LightBlue">Blue 1</Label>
  <Label Background="Yellow">Yellow 1</Label>
  <Label Background="Orange">Orange 1</Label>
</StackPanel>


Dock Panel

احتمالا به خاطر نامش، نحوه کارش را حدس زده اید. این پنل، اشیاء موجود را در 4 جهت و مرکز می‌چسباند. مشخص نمودن جهت چسبیده شدن هر کنترل توسط خاصیت DockPanel.Dock صورت می‌گیرد و مقدار Left، مقدار پیش فرض است. در صورتی که بخواهید المانی را در مرکز بچسبانید باید آن را به عنوان آخرین المان معرفی کرده و در Dock Panel مقدار خاصیت LastChildFill را با True برابر کنید.

<DockPanel LastChildFill="True">
    <Button Content="Dock=Top" DockPanel.Dock="Top"/>
    <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/>
    <Button Content="Dock=Left"/>
    <Button Content="Dock=Right" DockPanel.Dock="Right"/>
    <Button Content="LastChildFill=True"/>
</DockPanel>


به نحوه‌ی تعریف خاصیت DockPanel.Dock دقت کنید به این نوع خاصیت‌ها،  Attached Dependency Property (شاید در فارسی بتوانیم خاصیت‌های وابستگی متصل صدا بزنیم) می‌گویند. این خاصیت‌ها نوع خاصی از خاصیت‌های وابستگی هستند که به شما اجازه می‌دهند مقداری را به شیء‌ایی نسبت دهید که آن شیء چیزی در مورد آن نمی‌داند. بهترین مثال در مورد این ویژگی، پنل‌ها هستند که یکی از موارد استفاده‌ی از آن را در بالا می‌بینید. هر پنل می‌تواند تا بی نهایت المان فرزند داشته باشد که هر المان باید خواصش توسط پنل مشخص گردد. ولی اگر پنل ما تعداد زیادی فرزند داشته باشد، نوشتن خواص هر کدام از فرزندها داخل تگ پنل، کاری غیر ممکن است. اینجاست که این نوع خاصیت‌ها خودشان را نشان می‌دهند. پس به این نحو مقادیر، داخل کنترل هر تگ تعریف می‌شود ولی توسط پنل مورد استفاده قرار می‌گیرد. نحوه‌ی نوشتن این نوع خاصیت: ابتدا یک پیشوند از نوع تگ پنل را در ابتدا آورده و سپس بعد از .(نقطه) نام خاصیت را ذکر می‌کنیم.

نحوه‌ی تعریف این نوع خاصیت‌ها در یک کلاس به صورت زیر است که برای شیء یا پنل canvas می‌باشد:

public static readonly DependencyProperty TopProperty =
    DependencyProperty.RegisterAttached("Top", 
    typeof(double), typeof(Canvas),
    new FrameworkPropertyMetadata(0d,
        FrameworkPropertyMetadataOptions.Inherits));
 
public static void SetTop(UIElement element, double value)
{
    element.SetValue(TopProperty, value);
}
 
public static double GetTop(UIElement element)
{
    return (double)element.GetValue(TopProperty);
}
در مثال dockPanel بالا در هر طرف تنها یک المان قرار دادیم. برای قرار دادن المان‌های بیشتر در طرفین، تنها ذکر یک المان جدید با خاصیت Dockpanel.dock کافی است و الویت نمایش آن‌ها بر اساس ترتیب نوشتن تگها توسط شماست. مثال زیر این نکته را نشان می‌دهد:
    <Button Content="Dock=Top" DockPanel.Dock="Top"/>
        <Button Content="Dock=Bottom" DockPanel.Dock="Bottom"/>
        <Button Content="Dock=Left"/>
        <Button Content="Dock=Left2"/>
        <Button Content="Right2" DockPanel.Dock="Right"/>
        <Button Content="Dock=Right" DockPanel.Dock="Right"/>
        <Button Content="LastChildFill=True"/>



Wrap Panel
این پنل بسیار شبیه StackPanel هست ولی مثل آن اشیاء را در یک سطر یا ستون ادامه نمی‌دهد؛ بلکه با رسیدن به انتهای پنجره، سطر یا ستون جدیدی را آغاز می‌کند. در stack panel با پایان پنجره، ادامه اشیا آن قابل مشاهده نبود ولی در این شیء با اتمام و رسیدن به لبه‌ی پنجره، اشیاء در سر جدید (افقی) یا ستون جدید (عمودی) نمایش داده می‌شوند. این پنل‌ها می‌توانند در ساخت تب‌ها و نوار ابزار استفاده شوند.

Canvas Panel

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

شروع ترسیم یک شکل دو بعدی روی آن بر اساس دوتا از چهار "خاصیت‌های وابستگی متصل" صورت می‌گیرد که به شرح زیر هستند:

  • Canvas.LEFT
  • Canvas.RIGHT
  • Canvas.TOP
  • Canvas.BOTTOM

نمونه از کد نوشته شده آن به صورت زیر است:

<Canvas>
    <Rectangle Canvas.Left="40" Canvas.Top="31" Width="63" Height="41" Fill="Blue"  />
    <Ellipse Canvas.Left="130" Canvas.Top="79" Width="58" Height="58" Fill="Blue"  />
    <Path Canvas.Left="61" Canvas.Top="28" Width="133" Height="98" Fill="Blue" 
          Stretch="Fill" Data="M61,125 L193,28"/>
</Canvas>

ترتیب قرارگیری اشکال روی هم در canvas به ترتیبی انجام می‌گیرد که در XAML نوشته اید ولی می‌توان با استفاده از خاصیت Canvas.ZIndex این ترتیب را تغییر داد.

<Canvas>
    <Ellipse Fill="Green" Width="60" Height="60" Canvas.Left="30" Canvas.Top="20"    
             Canvas.ZIndex="1"/>
    <Ellipse Fill="Blue"  Width="60" Height="60" Canvas.Left="60" Canvas.Top="40"/>
</Canvas>
شکل زیر در سمت راست، نتیجه‌ی کد بالاست ولی بدون ذکر ZIndex شکل سمت چپ نتیجه‌ی کار خواهد بود.

ViewBox
شاید عده‌ای به سختی آن را یک Layout بدانند و بیشتر آن را یک کنترل معمولی می‌شناسند ولی وظیفه‌ی آن بسیار شبیه Layout هاست. خصوصیتی که این شیء دارد این است که با تغییر اندازه محیط برنامه به هر نحوی، یک تغییر مقیاس روی اشیاء داخل آن رخ می‌دهد و کنترل‌ها به همراه متون و هر چیزی که در درخت منطقی و بصری است Scale آن تغییر می‌یابند.
نمونه‌ی کد زیر را تست کنید تا تفاوت بین دو Button را ببینید:
  <StackPanel Orientation="Vertical">
        <Button Content="Test" />
        <Viewbox Stretch="Uniform">
            <Button Content="Test" />
        </Viewbox>
    </StackPanel>
نتیجه‌ی کار:

در بخش دوم Layout‌ها مبحث گرید و ساخت Layout اختصاصی و تعدادی از خاصیت‌ها را بررسی خواهیم کرد.

مطالب
کار با Visual Studio در ASP.NET Core
پیش نویس: این مقاله ترجمه شده فصل 6 کتاب Pro Asp.Net Core MVC2 می‌باشد.

کار با Visual Studio

در این مقاله، یکسری توضیحاتی در مورد ویژگی‌های کلیدی ویژوال استودیو به برنامه نویس‌های (توسعه دهنده‌های) پروژه‌های Asp.net Core MVC ارائه می‌دهیم.

 
ایجاد یک پروژه

در ابتدا یک پروژه‌ی وب جدید Asp.net core را به نام Working و بر اساس قالب Empty ایجاد می‌کنیم. سپس در کلاس startup، قابلیت MVC را فعال میکنیم (کدهای این قسمت، در فصل 5 کامل شرح داده شده‌است)
 

namespace Working
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            //app.Run(async (context) =>
            //{
            //    await context.Response.WriteAsync("Hello World!");
            //});
        }
    }
}

ایجاد مدل:

یک پوشه جدید را به نام Models ایجاد می‌کنیم و بعد در این پوشه یک کلاس جدید را به نام Product ایجاد می‌کنیم و کدهای زیر را در کلاس ایجاد شده قرار میدهیم (این قسمت در فصل 5 نیز شرح داده شده‌است):
namespace Working.Models
{
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}
برای ایجاد یک فروشگاه ساده از اشیاء محصول، من یک فایل کلاس را به نام SimpleRepository.cs به پوشه Models اضافه و از آن برای تعریف کلاس استفاده کردم.
 

namespace WorkingWithVisualStudio.Models
{
    public class SimpleRepository
    {
        private static SimpleRepository sharedRepository = new SimpleRepository();
        private Dictionary<string, Product> products = new Dictionary<string, Product>();
        public static SimpleRepository SharedRepository => sharedRepository;

        public SimpleRepository()
        {
            var initialItems = new[] { new Product { Name = "Kayak", Price = 275M }, new Product { Name = "Lifejacket", Price = 48.95M }, new Product { Name = "Soccer ball", Price = 19.50M }, new Product { Name = "Corner flag", Price = 34.95M } };
            foreach (var p in initialItems)
            {
                AddProduct(p);
            }
        }
        public IEnumerable<Product> Products => products.Values;
        public void AddProduct(Product p) => products.Add(p.Name, p);

    }
}
کلاس Stores، اشیا مدل را در حافظه ذخیره می‌کند. یعنی هر تغییری را که در Model داده باشید، زمانیکه نرم افزار متوقف یا Restart شود، از بین می‌رود. یک فروشگاه ناپیوسته برای مثال در این فصل کافی است. اما این رویکردی نیست که در بسیاری از پروژه‌های واقعی استفاده شود. در فصل 8 یک مثال را پیاده سازی می‌کنیم تا اطلاعات مدل Store را به صورت مداوم در بانک اطلاعاتی ذخیره کند.

نکته: من یک مشخصه (Property) استاتیک را به نام SharedRepository تعریف کردم که دسترسی به SimpleRepository را فراهم می‌کند و می‌تواند در طول برنامه از آن استفاده شود. این بهترین کار نیست، ولی میخواهم یک مشکل رایج را که در توسعه MVC روبرو میشوید، نشان دهم. من راه بهتری را برای کار با اجزای مشترک، در فصل 18 توضیح می‌دهم.


ایجاد Controller و View

در پوشه Controllers، یک فایل جدید را به نام HomeController.cs ایجاد می‌کنیم و کدهای زیر را در آن قرار میدهیم:
namespace WorkingWithVisualStudio.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View(SimpleRepository.SharedRepository.Products);
    }
}
این یکی Action Method ایی به نام Index است که اطلاعات مدل را دریافت می‌کند و برای View پیشفرض، جهت نمایش ارسال می‌کند. برای ایجاد View هم بر روی پوشه Views/Home راست کلیک کرده و یک View را به نام index.cshtml ایجاد کنید؛ با کدهای زیر:
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
</head>
<body>
    <table>
    <thead>
        <tr>
        <td>Name</td>
        <td>Price</td>
        </tr>
        </thead>
    <tbody>            @foreach (var p in Model)
    {<tr>
        <td>@p.Name</td>
        <td>@p.Price</td>
        </tr>}
        </tbody>
    </table>
</body>
</html>
این View شامل یک جدول است که از حلقه foreach Razor، برای ایجاد ردیف‌هایی برای هر شیء مدل استفاده می‌کند. جایی که هر ردیف، حاوی سلول‌هایی برای خواص نام و قیمت است. اگر شما برنامه کاربردی را اجرا کنید، نتایج حاصل را در شکل خواهید دید:

مدیریت بسته‌های نرم افزاری

دو نوع مختلف از بسته‌های نرم افزاری مورد نیاز برای Asp.Net Core MVC وجود دارند.

معرفی NuGet 

ویژوال استودیو به همراه یک ابزار گرافیکی برای مدیریت بسته‌های NET. است که در یک پروژه گنجانده شده‌است. برای باز کردن این ابزار، گزینه Management NuGet Packages for Solution را از منوی Tools ➤ NuGet Package Manager انتخاب کنید. به این ترتیب ابزار NuGet باز می‌شود و لیستی از بسته‌هایی که قبلا نصب شده‌اند، نمایش داده می‌شود؛ همانطور که در شکل زیر نشان داده شده‌است:
 


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

 
معرفی بسته‌ی MICROSOFT.ASPNETCORE.ALL

اگر شما از نسخه‌های قبلی Asp.Net Core استفاده کرده باشید، باید یک لیست طولانی از بسته‌های NuGet را به پروژه جدید خود اضافه نمایید. Asp.Net Core2 یک بسته‌ی متفاوت را به نام Microsoft.AspNetCore.All معرفی می‌کند.

پکیچ Microsoft.AspNetCore.All یک meta-package است که شامل تمام بسته‌های Nuget مورد نیاز Asp.net Core و MVC Framework است. یعنی شما دیگر نیازی به نصب تک به تک این نوع بسته‌ها ندارید و هنگامیکه برنامه خود را منتشر می‌کنید، هر بسته‌ای از بسته‌های Meta-package که مورداستفاده قرار نمی‌گیرند، حذف خواهند شد. البته این بسته در نگارش 2.1، قسمت All آن به App تغییر نام یافته‌است.
 
معرفی بسته‌های Nuget و موقعیت ذخیره سازی آن‌ها

ابزار NuGet لیست بسته‌های پروژه را در فایل projectname.csproj نگهداری می‌کند. در اینجا <projectname> با نام پروژه جایگزین میشود. برای مثال در پروژه فوق اطلاعات Nuget، در فایل WorkingWithVisualStudio.csproj ذخیره می‌شوند. ویژوال استودیو محتویات فایل csproj را در پنجره‌ی Solution Explorer نمایش نمی‌دهد. برای ویرایش این فایل، روی پروژه در پنجره‌ی Solution Explorer راست کلیک کنید و گزینه‌ی Edit WorkWithVisualStudio.csproj را از منوی باز شده، انتخاب کنید. ویژوال استودیو فایل را برای ویرایش باز می‌کند. فایل csproj یک فایل XML است و شما در آن عنصری را مانند قطعه کد زیر در آن می‌بینید که Asp.net Core Meta package را به پروژه اضافه می‌کند:
<ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
در اینجا یک بسته با نام و شماره نسخه مورد نیاز مشخص شده‌است. اگرچه بسته Meta-Package شامل تمام ویژگی‌های مورنیاز Asp.Net Core MVC می‌باشد، اما شما هنوز هم باید بسته‌های دیگری را به پروژه اضافه کنید تا بتوانید از ویژگی‌های اضافی خاص آن‌ها استفاده کنید. این بسته‌ها را می‌توان توسط رابط‌های خط فرمان و یا ابزار گرافیکی آن اضافه کرد. حتی شما می‌توانید فایل Csproj را به صورت مستقیم ویرایش کنید و ویژوال استودیو میتواند تغییرات بسته‌ها را شناسایی کرده، دانلود و نصب کند.
 


هنگامیکه از NuGet برای اضافه کردن یک بسته به پروژه‌ی خود استفاده می‌کنید، به صورت خودکار به همراه هر بسته‌ای که به آن وابستگی دارد، نصب می‌شود. شما می‌توانید بسته‌های Nuget و وابستگی‌های آن‌ها را در SolutionExpolrer از طریق گزینه‌ی Dependencies -> Nuget مشاهده کنید که هر یک از بسته‌های موجود در فایل csproj و وابستگی‌های آن‌ها را نشان می‌دهد. برای نمونه بسته Meta-Package ASP.Net Core دارای تعداد زیادی وابستگی است؛ برخی از آنها در شکل زیر دیده میشوند:


 
معرفی Bower

یک بسته Client-Side، شامل محتوایی است که به مشتری ارسال می‌شود؛ مانند فایل‌های جاوا اسکریپت، Css Stylesheets و یا تصاویر. از Nuget برای مدیریت این نوع فایل‌ها در پروژه نیز استفاده میشود. اما اکنون Asp.Net Core MVC پشتیبانی توکاری را از یک ابزار مدیریت بسته‌های سمت کاربر، به نام Bower نیز ارائه می‌دهد. Bower یک ابزار منبع باز ( Open Source ) است که در خارج از مایکروسافت و دنیای NET. توسعه داده شده و نگهداری می‌شود.

نکته: Bower به تازگی منسوخ شده اعلام گردیده‌است. ممکن است هشدارهایی را که ابزارهای جایگزین را پیشنهاد می‌کنند نیز مشاهده کنید. با این حال پشتیبانی از Bower با ویژوال استودیو یکپارچه شده‌است و در نگارش 2.1 ابزار مدیریت سمت کلاینت جدید دیگری را نیز بجای آن معرفی کرده‌اند.
 

معرفی لیست بسته‌های Bower

بسته‌های Bower از طریق فایل ویژه‌ی bower.json مشخص می‌شوند. برای ایجاد این فایل در پنجره Solution Explorer روی پروژه WorkingWithVisualStudio راست کلیک کنید و Add -> New Item را از منوی باز شده انتخاب کنید. سپس قالب مورد نظر Bower Configuration File را از Asp.net Core -> Web -> General  Category انتخاب نمائید؛ مانند تصویر زیر:
 


ویژوال استودیو نام bower.json را برای آن قرار میدهد. برروی ok کلیک می‌کنیم و یک فایل جدید، با محتویات پیشفرض زیر به پروژه اضافه میشود:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {}
}
به علاوه برای فایل Bower.json، تصویر زیر بسته‌های Client Side وابسته به Bower را نشان میدهد. از این قسمت برای اضافه کردن وابستگی‌های برنامه نیز استفاده میشود.


نکته: منبع بسته‌های Bower در لینک http://bower.io/search وجود دارد. شما می‌توانید بسته‌ها مورنظر را در اینجا جستجو و به پروژه اضافه کنید.

بعد از اینکه بسته‌ها نصب شدند، محتویات فایل bower.json به صورت زیر می‌باشد:
{
  "name": "asp.net",
  "private": true,
  "resolutions": {
    "jquery": "3.3.1"
  }
}

در ادامه بسته Bootstrap CSS به پروژه اضافه شده‌است. زمانیکه شما فایل Bower.json را ویرایش می‌کنید، ویژوال استادیو لیستی از نام بسته‌ها و نسخه‌های بسته‌های موجود را نمایش می‌دهد؛ مانند تصویر زیر:


در زمان نوشتن این مطلب، آخرین نسخه‌ی پایدار بسته بوت استرپ، 3،3،7 است. البته اگر در دقت کنید، در اینجا سه گزینه‌ی ارائه شده‌ی توسط ویژوال استودیو وجود دارند: 3.3.7 و 3.3.7^ و 3.3.7~. شماره نسخه می‌تواند در طیف وسیعی از روش‌های مختلف در فایل bower.json مشخص شود. مفیدترین آنها در جدول زیر شرح داده شده‌اند. استفاده از شماره نسخه صریح یک بسته، امن‌ترین راه برای مشخص کردن یک بسته است. این تضمین می‌کند که شما همیشه با همان نسخه کار می‌کنید؛ مگر اینکه عمدا فایل bower.json را برای پاسخ گویی به درخواست‌های دیگری به روز رسانی کنید:
  فرمت    توضیحات 
  3.3.7  بیان شماره مستقیم بسته نصب شده و تطبیق دقیق آن با شمار نسخه ، e.g ، 3.3.7 
  *  با استفاده از یک ستاره به Bower اجازه نصب آخرین نسخه را می‌دهد
3.3.7 =<3.3.7<
پیشوند یک شماره نسخه با < یا =< به Bower اجازه می‌دهد تا هر نسخه از بسته‌ای که بزرگتر یا بزرگتر مساوی آن نسخه‌ی معین است، نصب شود 
3.3.7 =>3.3.7
پیشوند یک شماره نسخه با > یا => به Bower اجازه می‌دهد تا هر نسخه از بسته‌ای را که کوچکتر یا کوچکتر و مساوی نسخه‌ی معین است، نصب شود 
  3.3.7~  پیشوند یک شماره نسخه با یک tilde (با کاراکتر ~ ) به نسخه‌هایی که دو شماره اولیه آن‌ها مشابه باشند، اجازه نصب میدهد؛ حتی اگر شماره آخر آن نسخه متفاوت باشد. مانند نسخه‌های 3.3.9 و 3.3.8 و اجازه نصب نسخه 3.4.0 را نمیدهد؛ چون شماره دوم آن متفاوت است.
  3.3.7^  پیشوند یک شماره نسخه با یک قلم (کاراکتر ^) به نسخه‌هایی که شماره اول آنها مشابه باشند، اجازه نصب می‌دهد؛ حتی اگر شماره دوم آن‌ها متفاوت باشد. مانند نسخه‌های 3.3.1 و 3.4.1 و 3.5.1 اما نسخه 4.0.0 اجازه نصب ندارد 
 
نکته: برای مثال در این کتاب، من فایل bower.json را مستقیما ایجاد و ویرایش می‌کنم. ویرایش این فایل ساده است و به شما کمک می‌کند تا اطمینان حاصل کنید که نتایج مورد انتظار را در صورت پیگیری به همراه داشته باشد. همچنین ویژوال استودیو ابزار گرافیکی را نیز برای مدیریت بسته‌های bower فراهم می‌کند. شما می‌توانید با کلیک راست بر روی فایل bower.json و انتخاب Manage Bower packages به منوی باز شده دسترسی داشته باشید. ویژوال استادیو فایلهای bower.json را برای تغییرات نظارت می‌کند و به صورت خودکار از ابزار Bower برای دانلود و نصب بسته‌ها استفاده می‌کند. هنگامیکه شما تغییرات فایل را ذخیره می‌کنید، ویژوال استودیو بسته‌ی BootStrap را دانلود می‌کند و در پوشه‌ی wwwroot/lib ذخیره می‌کند.


مانند Nuget نیز Bower وابستگی‌های مرتبط با بسته‌های اضافه شده‌ی به یک پروژه را مدیریت می‌کند. BootStrap برای دسترسی به برخی از ویژگی‌های پیشرفته، به JQuery که یک کتابخانه‌ی جاوا اسکریپتی است، تکیه می‌کند. به همین دلیل است که دو بسته را در شکل فوق نشان داده است. شما می‌توانید لیست بسته‌ها و وابستگی‌های آنها را به صورت باز شده در بخش مورد نظر در Solution Explorer مشاهده کنید.

به روزرسانی بسته Bootstrap

در ادامه کتاب، من از نسخه قبلی Bootstrap CSS framework استفاده می‌کنم. هنگامی که دارم این را می‌نویسم، تیم Bootstrap در حال توسعه‌ی نسخه‌ی 4 bootStrap است و چندین بار منتشر شده‌است. این نسخه‌ها به عنوان "آلفا" برچسب گذاری شده‌اند، اما کیفیت آن‌ها بالا است و برای استفاده در نمونه‌های این کتاب به اندازه کافی پایدار است. با توجه به انتخاب نوشتن این کتاب با استفاده از Bootstrap 3 و نسخه پیش از نسخه بوت استرپ 4 و به زودی بایگانی شدن آن، تصمیم گرفتم از نسخه جدید استفاده کنم؛ حتی اگر برخی از نام‌های کلاس‌ها که برای شیوه نامه‌های عناصر HTML استفاده می‌شوند، احتمالا قبل از انتشار نهایی تغییر یابند. این مورد به این معنا است که شما باید همان نسخه از Bootstrap را که برای گرفتن نتایج موردنظر از خروجی نیاز دارید، استفاده  کنید.

برای به روزرسانی بسته Bootstrap، شماره نسخه را در فایل bower.json تغییر دهید. مانند کد زیر:
{
  "name": "asp.net",
  "private": true,
  "dependencies": {
    "bootstrap": "4.0.0-alpha.6"
  }
}
زمانی که شما تغییرات فایل bower.json را ذخیره می‌کنید، ویژوال استودیو نسخه جدید BootStrap را دانلود می‌کند.
معرفی توسعه و کامپایل مداوم

توسعه نرم افزار وب اغلب می‌تواند یک فرآیند تکراری باشد، جایی که تغییرات کوچکی را به ویووها یا کلاس‌ها می‌دهید و برنامه را اجرا می‌کنید تا اثرات آن را آزمایش کنید. MVC و ویژوال استودیو همکاری می‌کنند تا از این رویکرد مداوم استفاده کنند تا تغییرات را سریع‌تر و آسان‌تر ببینید.

 اعمال تغییرات در Razor Views  
در زمان توسعه، تغییراتی که به Razor View اعمال میشوند، به محض رسیدن درخواست‌های HTTP، از مرورگر دریافت میشوند. برای اینکه ببینید چطور کار می‌کند، برنامه را با انتخاب گزینه Start Debugging از منوی Debug اجرا کنید و هنگامیکه یک برگه‌ی مرورگر باز شد و اطلاعات نمایش داده شد، تغییراتی را که در زیر نمایش میدهم در فایل Index.cshtml اعمال کنید.

@model IEnumerable<WorkingWithVisualStudio.Models.Product>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
</head>
<body>
    <h3>Products</h3>
    <table>
        <thead>
            <tr>
                <td>Name</td>
                <td>Price</td>
            </tr>
        </thead>
        <tbody>
            @foreach (var p in Model)
            {
            <tr>
                <td>@p.Name</td>
                <td>@($"{p.Price:C2}")</td>
            </tr>}
        </tbody>
    </table>
</body>
</html>
تغییرات را در فایل Index ذخیره می‌کنیم و صفحه وب را با استفاده از دکمه browser Reload مجددا بارگذاری می‌کنیم. تغییرات در View (یک عنوان و فرمت را برای مشخصه Price به عنوان ارز وارد کردیم) در مرورگر هم اعمال شده است؛ مانند تصویر زیر:



اعمال تغییرات در کلاس‌های #C

برای کلاس‌های #C، از جمله کنترلرها و مدل‌ها، دو رویکرد موجود را که از طریق آیتم‌های مختلف در منوی Debug انتخاب می‌شوند، شرح می‌دهم:

Start Without Debugging
تغییرات در کلاس‌ها در پروژه به صورت خودکار زمانیکه یک درخواست HTTP دریافت می‌شود، برای مشاهده‌ی یک تجربه‌ی توسعه‌ی پویا، کامپایل می‌شوند. در این حالت برنامه بدون امکانات دیباگ و اشکال‌زادیی اجرا می‌شود.

Start Debugging
به شما اجزا میدهد صریح تغییرات را کامپایل کنید و برنامه را اجرا کنید ، بررسی مشکلات هم در زمان اجرا پروژه انجام میگیرد.به شما اجرا بررسی و تجزیه و تحلیل هر گونه مشکل در کد را میدهد.

 
کامپایل خودکار کلاس ها

در طول توسعه عادی، این چرخه کامپایل سریع به شما اجازه می‌دهد تا فورا تاثیر تغییرات خود را ببینید؛ حالا می‌تواند این تغییر اضافه نمودن یک اکشن جدید و یا ویرایش نمایش اطلاعات یک Model باشد. برای ارائه‌ی این نوع از توسعه، ویژوال استودیو به محض رسیدن درخواست HTTP از مرورگر، تغییرات را دریافت و کلاس‌ها را به صورت خودکار کامپایل می‌کند. برای دیدن اینکه چگونه کار می‌کند، گزینه Start Without Debugging را از منوی Debug در ویژوال استودیو انتخاب کنید. هنگامیکه مرورگر داده‌های برنامه را نمایش می‌دهد، تغییرات زیر را در فایل Home controller ایجاد کنید:
namespace WorkingWithVisualStudio.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View(SimpleRepository.SharedRepository.Products
            .Where(p => p.Price < 50));
    }
}
در این تغییرات با استفاده از LINQ محصولات را فیلتر می‌کنیم به طوری که فقط کالاهایی را که price آنها کمتر از 50 است، نمایش داده می‌شوند. تغییرات را در فایل کلاس controller ذخیره کنید و پنجره مرورگر را دوباره باز کنید. بدون توقف و یا راه اندازی مجدد برنامه در ویژوال استادیو، درخواست HTTP از مرورگر باعث عملیات کامپایل میشود و برنامه با استفاده از تغییرات کلاس Controller، دوباره راه اندازی خواهد شد و نتیجه را در زیر میتوانید ببینیدکه محصولات Kayak را از جدول حذف می‌کند.

ویژگی کامپایل خودکار زمانی مفید است که همه چیز برنامه ریزی شود. مشکل این است که خطاهای کامپایلر، در زمان اجرا و در مرورگر بجای ویژوال استودیو نمایش داده می‌شوند. در این حالت زمانیکه یک مشکل وجود دارد، سخت می‌توان متوجه شد که چه مشکلی ایجاد شده است. برای مثال، کدهای زیر اضافه کردن یک مقدار Null را به مجموعه نمایش میدهد. 
namespace WorkingWithVisualStudio.Models
{
    public class SimpleRepository
    {
        private static SimpleRepository sharedRepository = new SimpleRepository();
        private Dictionary<string, Product> products = new Dictionary<string, Product>();
        public static SimpleRepository SharedRepository => sharedRepository;

        public SimpleRepository()
        {
            var initialItems = new[] { new Product { Name = "Kayak", Price = 275M }, new Product { Name = "Lifejacket", Price = 48.95M }, new Product { Name = "Soccer ball", Price = 19.50M }, new Product { Name = "Corner flag", Price = 34.95M } };
            foreach (var p in initialItems)
            {
                AddProduct(p);
            }
            products.Add("Error", null);
        }
        public IEnumerable<Product> Products => products.Values;
        public void AddProduct(Product p) => products.Add(p.Name, p);

    }
}
مشکلی مانند ورودی Null تا زمانیکه برنامه اجرا نشود، نمایش داده نمیشود. بارگذاری صفحه مرورگر باعث می‌شود کلاس SimpleRepository به صورت خودکار کامپایل شود و برنامه دوباره راه اندازی خواهد شد. هنگامیکه MVC نمونه‌ای از کلاس Controller را برای پردازش درخواست HTTP از مرورگر ایجاد می‌کند، سازنده HomeController کلاس SimpleRepository را ایجاد خواهد کرد که به نوبه خود سعی می‌کند مقدار Null اضافه شده در لیست را پردازش کند. مقدار Null باعث بروز یک مشکل می‌شود، اما مشخص نیست مشکل چیست. مرورگر یک پیام مفید را نمایش نمی‌دهد.
توانایی نمایش صفحات خطاها  
زمانیکه مشکلی در پنجره‌ی مرورگر ایجاد شد، می‌توان یک راهنمای با اطلاعات مفید را نمایش داد. این قابلیت را می‌توانید با فعال کردن نمایش صفحات انجام داد که باید در تنظیمات کلاس Startup تغییرات زیر را اعمال کنید.

namespace WorkingWithVisualStudio
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDeveloperExceptionPage();
            }
        }
    }
}
اگر پنجره مرورگر را دوباره بارگذاری کنید، فرآیند کامپایل خودکار به صورت خودکار برنامه را بازسازی می‌کند و یک پیام خطای مفید‌تری را در مرورگر ایجاد می‌کند. مانند تصویر زیر:

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

 
استفاده از Debugger

ویژوال استادیو از اجرای یک برنامه MVC با استفاده از Debugger نیز پشتیبانی می‌کند که اجازه می‌دهد برنامه برای بررسی وضعیت نرم افزار و دنبال کردن درخواستی که به برنامه ارسال میشود، متوقف و از این طریق، پیگیری شود. این مورد نیاز به یک سبک متفاوت از توسعه را دارد. زیرا تغییراتی را در کلاس‌های #C میدهیم، تا زمانیکه برنامه مجددا راه اندازی نشود، اعمال نمی‌شوند ( هرچند تغییرات Razor View هنوز هم به صورت خودکار اعمال میشوند). این سبک توسعه به همراه استفاده‌ی از ویژگی کامپایل خودکار نیست؛ اما Debugger ویژوال استودیو عالی است و می‌تواند بینش عمیق‌تری را در مورد نحوه‌ی کارکرد برنامه داشته باشد. برای اجرای برنامه با استفاده Debugger، در ویژوال استودیو از منوی Debug گزینه‌ی Start Debugging را انتخاب کنید. ویژوال استودیو کلاسهای #C در پروژه را قبل از اجرای برنامه کامپایل می‌کند. اما شما همچنان می‌توانید با استفاده از موارد موجود در منوی Build، کد خود را به صورت دستی نیز کامپایل کنید.

مثال فوق حاوی مقدار NULL است که سبب می‌شود یک NullReferenceException توسط کلاس SimpleRepository پرتاب شود. این حالت برنامه را قطع و کنترل اجرا را به توسعه دهنده منتقل می‌کند؛ همانطور که در شکل زیر نشان داده شده است



نکته: اگر Debugger خطا را نفهمد، گزینه‌ی Windows ➤ exception settings را از منوی Debugger ویژوال استودیو انتخاب کنید و اطمینان حاصل کنید که تمام انواع خطاهای در لیست خطاهای زمان اجرای زبان مشترک، تایید شده‌است.
تنظیم یک Break-point

Debugger عامل اصلی خطا را نمایش نمی‌دهد؛ تنها مکان آن‌را آشکار می‌کند. عبارتیکه ویژوال استودیو برجسته می‌کند نشان می‌دهد که این مشکل زمانی رخ می‌دهد که فیلتر کردن اشیاء با استفاده از LINQ انجام شود، اما یک کار کوچک لازم است تا از جزئیات کاسته شود و به علت اصلی برسد.
Breakpoint عبارتی است که به Debugger می‌گوید تا برنامه را متوقف کند و کنترل دستی برنامه را به برنامه نویس میدهد. شما می‌توانید وضعیت برنامه را بازبینی کنید و ببینید چه اتفاقی می‌افتد و به صورت اختیاری روند کاری را دوباره ادامه دهید.
برای ایجاد Breakpoint، روی عبارت راست کلیک کنید و در منوی باز شده، گزینه Breakpoint -> Insert Breakpoint را انتخاب کنید.

به عنوان مثال: یک Breakpoint به خط کد AddProduct در کلاس SimpleRepository اعمال کنید. همانطور که در شکل زیر نمایش داده میشود:
 


برنامه را اجرا کنید؛ با استفاده از Debug -> Start Debugging و یا با استفاده از Debug -> Restart برنامه را Restart می‌کنیم. در طی درخواست اولیه HTTP، برنامه اجرا میشود تا به نقطه‌ای که Break Point دارد برسد و در آنجا برنامه متوقف میشود. در این نقطه، شما می‌توانید از آیتم‌های منوی Debug ویژوال استودیو یا کنترل‌ها در بالای پنجره، برای کنترل اجرای برنامه استفاده کنید؛ یا از نمایش‌های مختلف Debugger موجود از طریق Debug -> Windows برای بررسی وضعیت برنامه استفاده می‌کنیم.
مشاهده مقادیر داده در ویرایشگر کد
رایج‌ترین استفاده Break Point، ردیابی مشکلات در کد شماست. قبل از اینکه بتوانید یک مشکل را رفع کنید، باید بدانید چه اتفاقی در حال رخ دادن است و یکی از ویژگیهای مفید ویژوال استودیو این است که توانایی مشاهده و کنترل ارزش متغیرها را درست در ویرایشگر کد، میدهد.
اگر اشاره‌گر ماوس را بر روی پارامتر p به متد AddProduct که توسط Debugger برجسته شده‌است، حرکت دهید، یک فرم ظاهر خواهد شد که ارزش فعلی p را نشان می‌دهد؛ همانطور که در شکل زیر نشان داده شده‌است. من یک نمونه بزرگ شده از محتویات فرم ظاهر شده را نمایش میدهم تا به راحتی بتوانید متن در آن را بخوانید.
 


این مورد ممکن است مؤثر به نظر نرسد، چون شیء داده در یک سازنده همانند BreakPoint تعریف شده‌است. اما این ویژگی‌ها برای هر متغیری کار می‌کند. شما می‌توانید مقادیر را مشاهده کنید تا مقادیر خود و فیلد آنها را ببینید. هر مقدار دارای یک دکمه پین​​ کوچک به سمت راست است. برای زمانیکه کد در حال اجراست، برای نظارت بر مقدار، از آن استفاده کنید.
اشاره‌گر ماوس را بر روی متغیر P قرار دهید و مرجع محصول را پین کنید. مرجع پیوست شده را باز کنید تا بتوانید نام و قیمت را نیز ببینید؛ مانند شکل زیر:
 


گزینه Continue را از منوی Debug در ویژوال استادیو انتخاب کنید تا برنامه ادامه پیدا کند. از آنجا که در برنامه حلقه Foreach وجود دارد، برنامه که دوباره اجرا میشود، وقتی مجددا به BreakPoint رسید، برنامه متوقف میشود. مقادیر پین شده در شکل زیر نشان میدهند که چگونه متغیر P و خواص آن تغییر می‌کنند.
 


استفاده از پنجره متغیرهای محلی ( Local Windows )

یکی از ویژگی‌های مرتبط، پنجره Locals است که با انتخاب گزینه‌ی منوی Debug ➤ Windows ➤ Locals باز می‌شود. پنجره‌ی Locals، مقدار متغیرها را به شکلی مشابه پنل پین شده نمایش می‌دهد، اما در اینجا تمام اشیاء محلی را نسبت به Break Point نمایش می‌دهد؛ همانطور که در شکل زیر نشان داده شده‌است:
 


هربار که Continue را انتخاب می‌کنید، اجرای برنامه ادامه یافته و یک شیء دیگر توسط حلقه foreach پردازش می‌شود.
اگر ادامه دهید، در زمان ویرایش کد، در هر دو پنجره Locals و در مقادیر پنل پین شده، شما مرجع Null را می‌بینید. برای کنترل اجرای برنامه، می‌توانید جریان را از طریق کد خود در دیباگر دنبال کنید و احساس کنید که چه اتفاقی می‌افتد.

برای غیرفعال کردن BreakPoint، روی  عبارت راست کلیک کنید و از منوی باز شده گزینه Delete BreakPoint را انتخاب کنید. برنامه را دوباره راه اندازی کنید و جدول داده ساده‌ای را که در شکل نشان داده شده، مشاهده خواهید کرد.


 
استفاده از Browser Link

ویژگی Browser Link می‌تواند روند توسعه را با قرار دادن یک یا چند مرورگر تحت کنترل ویژوال استودیو، ساده سازی کند. این ویژگی مخصوصا مفید است اگر شما نیاز به دیدن اثر تغییرات را در طیف وسیعی از مرورگرها دارید. قابلیت Browser Link با و یا بدون Debugger کار می‌کند و به این معنا است که می‌توانیم هر فایلی را در پروژه تغییر دهیم و تاثیر تغییر را بدون نیاز به تغییری در مرورگر مشاهده کنیم.

 
راه اندازی BrowserLink

برای فعال کردن Browser Link باید در کلاس Startup، تنظیمات را تغییر دهید. مانند کد زیر:

namespace WorkingWithVisualStudio
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}


استفاده از Browser Link

برای درک اینکه Browser Link چگونه کار می‌کند، در ویژوال استودیو گزینه Start Without Debugging را از منوی Debug انتخاب می‌کنیم. ویژوال استودیو برنامه را اجرا می‌کند و یک برگه جدید مرورگر را برای نمایش نتیجه باز می‌کند. با بازبینی HTML ارسال شده به مرورگر، شما خواهید دید که حاوی بخش دیگری مانند این است:
 

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
</head>
<body>
    <h3>Products</h3>
    <table>
        <thead>
            <tr><td>Name</td><td>Price</td></tr>
        </thead>
        <tbody>
            <tr><td>Lifejacket</td><td>&#xA3;48.95</td></tr>
            <tr><td>Soccer ball</td><td>&#xA3;19.50</td></tr>
            <tr><td>Corner flag</td><td>&#xA3;34.95</td></tr>
        </tbody>
    </table>
    <!-- Visual Studio Browser Link -->
    <script type="application/json" id="__browserLink_initializationData">
        {"requestId":"968949d8affc47c4a9c6326de21dfa03","requestMappingFromServer":false}
    </script>
    <script type="text/javascript" src="http://localhost:55356/d1a038413c804e178ef009a3be07b262/browserLink" async="async"></script> <!-- End Browser Link -->
</body>
</html>
نکته: اگر قسمت اضافی را نمی‌بینید، لینک مرورگر را از منوی نشان داده شده‌ی در شکل زیر فعال کنید و مرورگر را دوباره بارگذاری کنید.


ویژوال استادیو یک جفت عناصر اسکریپت را به HTML فرستاده شده‌ی به مرورگر اضافه می‌کند که برای بازکردن یک اتصال طولانی مدت HTTP با سرور برنامه کاربردی است؛ تا زمانیکه ویژوال استودیو مجددا برنامه را ری‌استارت کند. کد زیر تغییر در فایل Index و تاثیر استفاده از Browser Link را نشان میدهد.
 

@model IEnumerable<WorkingWithVisualStudio.Models.Product>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
</head>
<body>
    <h3>Products</h3>
    <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p>
    <table>
        <thead>
            <tr>
                <td>Name</td>
                <td>Price</td>
            </tr>
        </thead>
        <tbody>
            @foreach (var p in Model)
            {
                <tr>
                    <td>@p.Name</td>
                    <td>@($"{p.Price:C2}")</td>
                </tr>}
        </tbody>
    </table>
</body>
</html>
تغییر در فایل View را ذخیره کنید و Refresh Linked Browsers را از منوی Browser Link در نوار ابزار ویژوال استودیو انتخاب کنید؛ همانطور که در شکل نشان داده شده است.  اگر Browser Link کار نمی‌کند، بارگیری مجدد مرورگر یا راه اندازی مجدد ویژوال استادیو را امتحان کنید).
 


کد جاوا اسکریپتی که در HTML ارسال شده به مرورگر جاسازی شده، صحفه را دوباره بارگذاری می‌کند؛ برای دیدن تاثیرات کد اضافه شده که اضافه کردن  یک timestamp ساده است.
 
نکته: عناصر اسکریپت Browser Link فقط در پاسخ‌های موفق جاسازی شده است. به این معنا که اگر یک خطا هنگام کامپایل در هنگام اجرا کردن یک Razor View یا مدیریت یک درخواست ایجاد شود، اتصال بین مرورگر و ویژوال استودیو از بین میرود و شما بعد از حل مشکل باید صفحه را مجدد بارگذاری کنید.

 
استفاده از مرورگرهای متعدد

Browser Link می‌تواند برای نمایش یک برنامه در مرورگرهای متعددی به طور همزمان استفاده شود و می‌تواند زمانی مفید باشد که شما می‌خواهید تفاوت‌های پیاده سازی را بین مرورگرهای مختلف کنترل کنید و یا ببینید که چگونه یک برنامه بر روی ترکیبی از مرورگرهای دسکتاپ و تلفن همراه ارائه می‌شود.
برای انتخاب مرورگرهایی که استفاده می‌شوند، مرورگر را با استفاده از دکمه IIS Express در نوار ابزار ویژوال استودیو، انتخاب کنید؛ همانطور که در شکل زیر نشان داده شده است.
 


ویژوال استودیو لیستی از مرورگرهایی را که در مورد آنها اطلاعاتی دارد، نمایش میدهد. در عکس زیر مرورگرهایی را که من در سیستم خود نصب کرده‌ام، نشان می‌دهد. برخی از آنها با ویندوز مانند Internet Explorer و Edge نصب می‌شوند.

 
ویژوال استادیو معمولا مرورگرهای رایجی را که نصب میشوند، نمایش میدهد. اما شما می‌توانید با استفاده از دکمه‌ی Add، برای اضافه کردن مرورگری که به صورت خودکار لیست نشده نیز استفاده کنید. همچنین می‌توانید ابزار تست شخص ثالث مانند Browser Stack را نیز راه اندازی کنید که مرورگرها را بر روی سرویس‌های ابری میزبان ( cloud-hosted ) و ماشین‌های مجازی اجرا می‌کند.

من سه مرورگر را در شکل انتخاب کردم: Chrome ، Internet Explorer و Edge. با کلیک بر روی دکمه Browse، فعالیت هر سه مرورگر شروع می‌شود و باعث می‌شود URL مثال برنامه را بارگذاری کند؛ همانطور که در شکل نشان داده شده است.
 


با استفاده از منوی Browser Link Dashboard، شما می‌توانید ببینید که چه مرورگرهایی در Browser Link انتخاب شده‌اند. داشبورد آن نشانی اینترنتی نمایش داده شده توسط هر مرورگر را نشان می‌دهد و در اینجا هر مرورگر را می‌توان به صورت جداگانه رفرش کرد.
 


آماده سازی جاوا اسکریپت و CSS برای استقرار

هنگامی که Client-Side بخشی از یک برنامه وب را ایجاد می‌کنید، معمولا تعدادی از فایل‌های جاوا اسکریپت و CSS سفارشی را تهیه می‌کنید که برای تکمیل آن‌ها، از بسته‌های نصب شده‌ی توسط Bower استفاده می‌شود. این فایل‌ها نیاز به پردازش دارند تا آنها را برای تحویل در یک محیط تولید، بهینه سازی کنند تا تعداد درخواستهای HTTP و میزان پهنای باند شبکه مورد نیاز برای ارسال آنها به مشتری، به حداقل برسد. این فرآیند به عنوان بسته بندی شناخته می‌شود.
 

فعال کردن تحویل محتوای استاتیک

ASP.Net Core شامل پشتیبانی از ارائه فایل‌های استاتیک از پوشه wwwroot به مشتریان است. اما این امکان به صورت پیشفرض در زمان ایجاد یک پروژه‌ی خالی جدید فعال نیست و شما باید با قرار دادن عبارتی در فایل StartUp آن را فعال کنید؛ مانند کد زیر:
 

namespace WorkingWithVisualStudio
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
                app.UseStaticFiles();
                app.UseDeveloperExceptionPage();
            }

            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello World!");
            });
        }
    }
}


اضافه کردن محتوای استاتیک به پروژه

برای نشان دادن فرآیند بسته بندی، من نیاز به اضافه کردن تعدادی محتوای استاتیک به پروژه و یکی کردن آن‌ها با برنامه‌ی نمونه را دارم. برای این منظور ابتدا یک پوشه‌ی جدید را به نام wwwroot/css ایجاد کنید که محل متداولی برای فایل‌های سفارشی CSS است. من فایلی را به نام First.css با استفاده از قالب آیتم Style Sheet اضافه کردم؛ همانطور که در شکل زیر نشان داده شده است. قالب Style Sheet در مسیر Asp.Net Core -> Web -> Content Section وجود دارد.
 


فایل First.Css را ویرایش کنید و محتوای زیر را در آن قرار دهید.
h3 {
}

table, td {
    border: 2px solid black;
    border-collapse: collapse;
    padding: 5px;
}
من این روند را تکرار کردم و یک فایل دیگر را نیز به نام second.css در پوشه wwwroot/css ایجاد کردم.

فایل‌های جاوا اسکریپت معمولا در پوشه wwwroot/js قرار میگیرند. من این پوشه را ایجاد کردم. فایل‌های جاوا اسکریپت را می‌توانید در مسیر Asp.Net Core -> Web -> Script انتخاب کنید. همانطور که در شکل زیر نشان داده شده است.


من کد جاوا اسکریپتی ساده زیر را به این فایل جدید اضافه کردم؛ همانطور که در لیست نشان داده شده است.
document.addEventListener("DOMContentLoaded", function ()
{
    var element = document.createElement("p");
    element.textContent = "This is the element from the third.js file";
    document.querySelector("body").appendChild(element);
});

من به بیش از یک فایل جاوا اسکریپت نیاز دارم. بنابراین فایل دیگری را به نام fourth.js نیز در پوشه wwwroot ایجاد می‌کنم و محتوای زیر را در آن قرار میدهم.
document.addEventListener("DOMContentLoaded", function ()
{
    var element = document.createElement("p");
    element.textContent = "This is the element from the fourth.js file";
    document.querySelector("body").appendChild(element);
});


به روز رسانی View

گام نهایی، به روز رسانی فایل Index.cshtml برای استفاده از Css و فایل جاوا اسکریپت است. کد‌های آن در زیر نشان داده شده است:
@model IEnumerable<WorkingWithVisualStudio.Models.Product>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
    <link rel="stylesheet" href="css/first.css" />
    <link rel="stylesheet" href="css/second.css" />
    <script src="js/third.js"></script>
    <script src="js/fourth.js"></script>
</head>
<body>
    <h3>Products</h3>
    <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p>
    <table>
        <thead>
            <tr>
                <td>Name</td>
                <td>Price</td>
            </tr>
        </thead>
        <tbody>
            @foreach (var p in Model)
            {
                <tr>
                    <td>@p.Name</td>
                    <td>@($"{p.Price:C2}")</td>
                </tr>}
        </tbody>
    </table>
</body>
</html>
اگر برنامه کاربردی را اجرا کنید، محتویات نشان داده شده‌ی در شکل زیر را مشاهده خواهید کرد. محتوای موجود توسط شیوه نامه‌های CSS شبیه سازی شده است و کد جاوا اسکریپتی جدیدی را اضافه کرده است.


یکی کردن فایل‌های سمت کلاینت در برنامه‌های MVC

در حال حاضر چهار فایل استاتیک وجود دارند و مرورگر باید چهار درخواست را برای دریافت فایل‌های استاتیک ایجاد کند و هر یک از این فایل‌ها نیازمند پهنای باند بیشتری است که باید به مشتری تحویل داده شود؛ زیرا آنها حاوی فضای سفید و نام متغیرها هستند که برای توسعه دهنده‌ها معنا دار هستند؛ اما برای مرورگرها اهمیتی ندارند.
ترکیب فایل‌هایی هم نوع، تلفیق نامیده می‌شود و در آن کار ساختن فایل‌ها به صورتی کوچکتر انجام می‌شود. هر دوی این کارها در برنامه Asp.Net Core MVC توسط  Bundler & Minifier مخصوص ویژوال استودیو انجام میشود.


نصب افزونه‌های ویژوال استودیو

اولین قدم برای نصب افزونه، انتخاب از منوی Tools -> Extensions and Update و کلیک بر روی مجموعه Online است تا افزونه‌های ویژوال استودیو را در مجموعه نمایش بدهد. نام افزونه را در جعبه جستجوی در گوشه‌ی سمت راست بالای پنجره وارد کنید؛ همانطور که در شکل زیر نشان داده شده است. محل نصب افزونه را مشخص می‌کنیم و بر روی دانلود کلیک می‌کنیم تا آن را به ویژوال استودیو اضافه کند. ویژوال استودیو را مجدد راه اندازی کنید تا فرآیند نصب تکمیل شود.


دسته بندی و یکی کردن فایل‌ها

پس از نصب افزونه، ویژوال استودیو را مجددا راه اندازی کنید و پروژه نمونه را باز کنید. با افزودن افزونه، می‌توانید چندین فایل هم نوع را در Solution Explorer انتخاب کنید. آنها را با یکدیگر ترکیب کرده و محتویات آنها را کوچکتر کنید. به عنوان مثال فایل‌های First.css و Second.css را در Solution Explorer را انتخاب و کلیک راست کرده و سپس Bundler & Minifier -> Bundle and Minify Files را از منوی باز شده انتخاب کنید . همانطور که در شکل زیر نشان داده شده است.
 


فایل خروجی را با عنوان bundle.css ذخیره کنید. در Solution Explorer یک بسته جدید ایجاد میشود. اگر شما این فایل را باز کنید، خواهید دید که محتویات هر دو فایل CSS جداگانه ترکیب شده‌اند و تمام فضای سفید آن‌ها حذف شده‌است. البته شما نمی‌خواهید به طور مستقیم با این فایل کار کنید؛ اما این فایل کوچکتر است و فقط یک اتصال HTTP را برای ارائه CSS styles به مشتری نیاز دارد.

مراحل قبل را برای فایل‌های third.js و fourth.js تکرار کنید تا فایل‌های جدید bundle.js و bundle.min.js در پوشه wwwroot ایجاد شوند.

احتیاط: اطمینان حاصل کنید که فایل‌ها را به ترتیبی که توسط مرورگر بارگیری می‌شوند، انتخاب کنید تا ترتیب دستورات Style‌ها یا دستورات کد را در فایل‌های خروجی حفظ کنید. به عنوان مثال دقت کنید که فایل third.js قبل از فایل fourth.js انتخاب شده باشد تا مطمئن باشید دستورات به ترتیب و به درستی اجرا می‌شوند.
کد زیر، عناصر پیوند فایل‌های جداگانه‌ای را که باید در فایل Index.cshtml قرار گیرند، نمایش میدهد:
@model IEnumerable<WorkingWithVisualStudio.Models.Product>
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>>Working with Visual Studio</title>
    <link rel="stylesheet" href="css/bundle.min.css" />
    <script src="js/bundle.min.js"></script>
</head>
<body>
    <h3>Products</h3>
    <p>Request Time: @DateTime.Now.ToString("HH:mm:ss")</p>
    <table>
        <thead>
            <tr>
                <td>Name</td>
                <td>Price</td>
            </tr>
        </thead>
        <tbody>
            @foreach (var p in Model)
            {
                <tr>
                    <td>@p.Name</td>
                    <td>@($"{p.Price:C2}")</td>
                </tr>}
        </tbody>
    </table>
</body>
</html>
اگر برنامه را اجرا کنید، هیچ تغییر بصری وجود نخواهد داشت؛ اما فایلهای آن یکی شده‌اند و با حجم کمتر و با تعداد اتصالات کمتری از سرور دریافت می‌شوند.

همان زمان که عملیات جمع آوری و یکی کردن را انجام می‌دهید، رکورد عملیات انجام شده را در فایلی به نام bundleconfig.json در پوشه‌ی wwwroot پروژه نگهداری می‌کند. در اینجا یک نمونه از فایل تولیدی را مشاهده می‌کنید:
[
  {
    "outputFileName": "Views/wwwroot/css/bundle.css",
    "inputFiles": [
      "Views/wwwroot/css/First.css",
      "Views/wwwroot/css/second.css"
    ]
  },
  {
    "outputFileName": "Views/wwwroot/js/bundle.js",
    "inputFiles": [
      "Views/wwwroot/js/fourth.js",
      "Views/wwwroot/js/third.js"
    ]
  }
]
 

خلاصه
در این بخش من توضیحاتی را در مورد ویژگی‌هایی که ویژوال استودیو برای طراحی برنامه‌های وب به توسعه دهنده‌ها ارائه میدهد، شرح دادم که شامل کامپایل خودکار کلاس‌ها، Browser Link و یکی کردن فایل‌های سمت کلاینت ( bundling and minification ) بود.