مطالب
معرفی کتابخانه PdfReport
مدتی هست که در حال تهیه کتابخانه‌ی گزارش سازی مبتنی بر iTextSharp هستم و عمده‌ی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویس‌های WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی می‌توانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عامل‌ها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار می‌گیرد و جزئی از آن می‌شود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانه‌های معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاری‌های مرتبط با هر کدام از کتابخانه‌های یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحه‌ای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آن‌ها اشاره‌ای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمن‌های برنامه نویسی مطرح می‌کنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آن‌ها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.


اما چرا PDF؟

استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده می‌شود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینه‌ای می‌توان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونت‌های شکیل‌تر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را می‌توان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عامل‌ها پشتیبانی می‌شود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عامل‌ها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،‌اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت می‌کند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامه‌های رایگان برای مشاهده فایل‌های PDF زیاد است.
- تمام صفحات گزارش را در یک فایل می‌توان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمی‌شود مثلا یک گزارش 100 صفحه‌ای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست می‌دهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آن‌را چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه می‌شود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتاب‌های الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتاب‌ها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین می‌توان به فایل‌های PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامه‌های PDF خوان معتبر گزارش داده شده و می‌توان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایل‌ها و تصاویر قرار داده شده در آن پشتیبانی می‌کند.
- از گرافیک برداری پشتیبانی می‌کند.


مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژه‌ای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده می‌کند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایل‌های اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده می‌کنید، محدودیتی را برای شما ایجاد نخواهد کرد.


کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره می‌توانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.


نحوه تهیه اولین گزارش، با کتابخانه PdfReport

الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژه‌های VS.NET قابل استفاده است، می‌توان از این پروژه Class library به عنوان کلاس‌های پایه قابل استفاده در انواع و اقسام پروژه‌های مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.

ب) آخرین نگارش فایل‌های مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلی‌های موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن می‌باشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.

ج) کلاس‌های زیر را به آن اضافه کنید:
using System.Web;
using System.Windows.Forms;

namespace PdfReportSamples
{
    public static class AppPath
    {
        public static string ApplicationPath
        {
            get
            {
                if (isInWeb)
                    return HttpRuntime.AppDomainAppPath;

                return Application.StartupPath;
            }
        }

        private static bool isInWeb
        {
            get
            {
                return HttpContext.Current != null;
            }
        }
    }
}
از این کلاس برای مشخص سازی محل ذخیره سازی فایل‌های نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده می‌کنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.

در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظه‌ای تعریف خواهیم کرد:
using System;

namespace PdfReportSamples.IList
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string LastName { set; get; }
        public long Balance { set; get; }
        public DateTime RegisterDate { set; get; }
    }
}
اکنون کلاس اصلی گزارش ما به صورت زیر خواهد بود:
using System;
using System.Collections.Generic;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.IList
{
    public class IListPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.ClassicTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 200; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
            })
            .Export(export =>
            {
                export.ToExcel();
                export.ToCsv();
                export.ToXml();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf"));
        }
    }
}
و برای استفاده از آن:
var rpt = new IListPdfReport().CreatePdfReport();
// rpt.FileName


برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثال‌های همراه سورس پروژه در مخزن کد آن مراجعه نمائید.

توضیحات بیشتر:

- در قسمت  DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم می‌شوند.
- سپس نیاز است قلم‌های مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو  قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار می‌گیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص می‌شود. در مورد ساخت Footer سفارشی در قسمت‌های بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمت‌های بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیف‌های گزارش مشخص می‌شود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالب‌‌های سفارشی به مرور در قسمت‌های بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین می‌شود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستون‌های گزارش به کمک متد ColumnsWidthsType مشخص می‌شود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص می‌شود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار می‌گیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمت‌های بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسب‌های جمع‌های پایین صفحات مشخص می‌شود.
- در قسمت MainTableColumns، ستون‌هایی را که علاقمندیم در گزارش ظاهر شوند، قید می‌کنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده می‌کنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستون‌های آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، می‌توان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، می‌توان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجی‌های دلخواه مورد نظر را می‌توان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آن‌ها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.

 لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
مطالب
افزونه مدیریت فایل های رایگان Roxy FileMan برای TinyMce و CkEditor
در این مقاله سعی در پیاده سازی Roxy FileMan  در Asp.net mvc داریم. البته خود وب سایت آن (اینجا) برای .net Framework 4.5 پروژه‌ایی را آماده کرده است که بنده بنا دارم همان پروژه را توضیح دهم.
قبل از شروع به کار، ابتدا نمایی از این ابزار مدیریت فایل (File Manager) را ملاحظه فرمایید:


1- ابتدا یک پروژه‌ی جدید MVC می‌سازیم
2- جهت بخش سمت سرور، کد هندلری به نام main.ashx نوشته شده است که در فولدر asp_net قرار دارد و در ادامه به توضیح آن خواهم پرداخت. با هندلر بیشتر آشنا شوید.
3- پیشنهاد می‌شود جهت راحتی کار و انتقال بین پروژه‌های خود در فولدر scripts فولدری را به نام  RoxyFileManager بسازید. این کار سبب می‌شود جهت پروژه‌های مختلف فقط فولدر RoxyFileManager را بین پروژه‌های خود کپی و پیست کنید.
4- از داخل پروژه دانلود شده، فولدر های asp_net ،css، Images، js، lang را به پروژه‌ی جدید کپی می‌کنیم.

 
5- اکنون به سراغ web.config می‌رویم. موارد قرمز ضروری هستند که به web,config اضافه شوند.


6- کل مدیریت فایل در فایل index.html می‌باشد و اگر تمایل به تغییر عنوان، درج لوگوی خودتان و ... را دارید به این فایل مراجعه کنید.
7- فایل conf.json، همانطور که از نام این فایل برداشت می‌شود، تنظیمات را می‌توان به فرمت json در این فایل جستجو کرد. مهمترین تنظیمات، بحث تغییر زبان می‌باشد که می‌توان با تعویض قسمت LANG از en به هر زبان دلخواهی که داخل پوشه‌ی lang می‌باشد، آن‌را تغییر داد که البته بنده آن‌را به زبان فارسی هم ترجمه کرده‌ام که در فولدر lang جهت استفاده موجود می‌باشد.
8- اکنون به فولدر asp_net می‌رویم و با handler بیشتر آشنا می‌شیم. در خط‌های اول، به متغییر confFile بر می‌خوریم که آدرس فایل conf.json را داده است.
9- آدرس دایرکتوری اصلی را در متد GetFilesRoot جستجو کنید. (من معمولا در پروژه‌های خود، فولدری دارم به نام uploads که داخل آن تمام فولدرهام که مربوط به ماژول‌های مختلف است، نگه داری می‌شوند و خود کاربر آپلود می‌کند. اکنون برای این پروژه می‌توان یک فولدر جدید باز کرد و به آن آدرس داد تا در روند پروژه‌ی شما اختلالی ایجاد نشود. من نام آن فولدر را web گذاشته‌ام)
نکته جالب همین جاست که شما به کمک این آدرس دهی از source پروژه می‌توانید خیلی کارها را انجام دهید. مثلا در یک پروژه می‌خواهید به فولدرها طبق نقشی که کاربران  در سیستم دارند دسترسی بدهید تا فایل‌ها را آپلود کنند و ... به راحتی می‌توانید با کمی کد نویسی این تغییرات را در متد GetFilesRoot اعمال کنید.
اگر بخواهید مدیریت فایل را بر اساس زبان سیستم تغییر دهید کافیست، یک فایل conf.json دیگر ایجاد کنید و در سورس به ازای هر زبان، آن فایل conf.json را لود کنید. البته در هر فایل شما باید زبان را تغییر دهید.
راست چین کردن این ادیتور را در اسرع وقت انجام می‌دهم و برای شما آپلود می‌کنم. تا کاملا نسخه‌ی بومی سازی آن در اختیار شما قرار گیرد.
10- اکنون به سراغ تنظیمات Tiny mce می‌رویم.
خوب، ابتدا با دستور Install-Package TinyMCE در Package Manager Console و انتخاب سورس Nuget آخرین نسخه را نصب می‌کنیم. در حال حاضر آخرین نسخه نگارنده 4.2.5 می باشد.
کل این تنظیمات در فایل Tinymce.html می‌باشد (این فایل در پروژه شما می‌تواند همان view‌ها و partial view‌های شما باشد که تنظیمات اولیه tinymce را می‌نویسید). حال بررسی مواردی که به tinymce اضافه می‌شوند تا با مدیریت فایل ادغام شود:


و اسکریپتی جهت افزونه‌ی مدیریت فایل Roxy FileMan را در پایین متد اضافه می‌کنیم.


خوب، اکنون فایل Tinymce.html  را اجرا کنید و نتیجه را ملاحظه بفرمایید.
Roxy FileMan را می‌توان با CKEditor هم سازگار کرد، که جهت اطلاعات بیشتر  اینجا  را مطالعه فرمایید.
فایل پروژه: RoxyFileManager.rar
مطالب
اهمیت Controller های ساده در ASP.NET MVC
Controller‌ها به نوعی رابط بین View و Model هستند. ساده ترین محل برای قرار دادن کد‌های تصمیم گیری (decision-making code) ، قرار دادن منطق تجاری و یا فراهم ساختن داده برای View مثل ایجاد یک لیست از Select List برای یک DropDownList می‌باشند. اما انجام این کار‌ها به نرم افزار ما پیچیدگی تحمیل می‌کند. Controller‌ها باید در طول زمان توسعه‌ی یک نرم افزار کم حجم و سبک باقی بمانند. در این مطلب  بحث شد که یکی از اهداف استفاده از ASP.NET MVC نوشتن نرم افزار هایی تست پذیر می‌باشد. نوشتن آزمون واحد و نگهداری Controller هایی که مسئولیت زیادی بر عهده دارند سخت می‌باشد. Controller‌ها باید به نحوی توسعه پیدا کنند که نگهداری آن‌ها ساده باشد ، تست پذیر باشند و اصل SRP  را رعایت کرده باشند.
نگهداری آسان
کدی که درک آن سخت باشد ، تغییر دادن آن نیز سخت است ، هنگامی که درک کدی سخت باشد زمینه برای به وجود آمدن خطاها ، دوباره کاری و سردرد فراهم می‌شود. هنگامی که مسئولیت یک Action به صورت شفاف مشخص نباشد و انواع و اقسام کار‌ها به آن سپرده شده باشد تغییر در آن سخت می‌شود ،  ممکن است این تغییر باید چند جای دیگر هم داده شود در نتیجه فاز نگهداری هزینه و زمان اضافی به نرم افزار تحمیل می‌کند.
تست پذیری
 بهترین راه برای اطمینان از این که درک و تست پذیری سورس کد ما ساده هست انجام و تمرین توسعه‌ی تست محور (TDD) می‌باشد. هنگامی که از روش TDD استفاده می‌کنیم با سورسی کدی کار می‌کنیم که هنوز وجود ندارد. در این مرحله از به وجود آمدن کلاس هایی که تست آن‌ها دشوار یا غیر ممکن است (به دلیل داشتن مسئولیت‌های اضافی) از جمله Controller‌ها جلوگیری می‌شود. نوشتن آزمون‌های واحد برای Controller‌های کم حجم ساده می‌باشد. 
تمرکز بر روی یک مسئولیت
 بهترین راه برای ساده سازی Controller‌ها گرفتن مسئولیت‌های اضافی از آن می‌باشد  به Action زیر توجه کنید :
  
public RedirectToRouteResult Ship(int orderId)
{
   User user = _userSession.GetCurrentUser();
   Order order = _repository.GetById(orderId);
   if (order.IsAuthorized)                                
   {
      ShippingStatus status = _shippingService.Ship(order);
      if (!string.IsNullOrEmpty(user.EmailAddress))      
      {
         Message message = _messageBuilder
            .BuildShippedMessage(order, user);
         _emailSender.Send(message);
      }
      if (status.Successful)
      {
         return RedirectToAction("Shipped", "Order", new {orderId});
      }
   }
   return RedirectToAction("NotShipped", "Order", new {orderId});
}
 این Action کار زیادی انجام می‌دهد ، تقریبا می‌توان تعداد مسئولیت‌های این Action را با شمارش تعداد If‌ها پیدا کرد . مسئولیت تصمیم گیری درباره این که آیا Order مورد نظر آماده برای تحویل است یا خیر به این Action سپرده شده ، همچنین تصمیم گیری درباره اینکه آیا کاربر دارای آدرس ایمیل جهت ارسال ایمیل می‌باشد یا خیر به این Action سپرده شده است. منطق‌های domain logic و business logic نباید در کلاس‌های presentation همانند Controller‌ها قرار داده شود. تست و نگهداری کدی مثل کد بالا دشوار خواهد بود. Refactoring که باید در این Code اعمال شود Refactor Architecture by Tiers  نام دارد. این Refactoring به توسعه دهنده می‌گوید که منطقی که در لایه‌ی نمایش (Presentation) پیاده کرده را به لایه‌ی Business انتقال دهد. پس از انتقال منطق کد بالا به OrderShippingService ، کد Action ما ساده‌تر می‌شود : 
public RedirectToRouteResult Ship(int orderId)
{
   var status = _orderShippingService.Ship(orderId);
   if (status.Successful)
   {
      return RedirectToAction("Shipped", "Order", new {orderId});
   }
   return RedirectToAction("NotShipped", "Order", new {orderId});
}
پس از انتقال منطق تجاری به محل مناسب خودش تنها مسئولیتی که برای برای Controller باقی مانده این است که کاربر را به کجا Redirect کند. پس از این Refactoring علاوه برا اینکه مسئولیت‌ها در جای مناسب خود قرار گرفتند ، اکنون می‌توان به سادگی منطق کار را بدون تحت تاثیر قرار گرفتن کد‌های لایه‌ی نمایش تغییر داد. 
در آینده به تکنیک‌های ساده سازی Controller‌‌ها خواهیم پرداخت.
مطالب
ذخیره و بازیابی فایل در Mongodb (بخش سوم)
در قسمت‌های پیشین (^ ،^ ) در مورد عملیات CRUD در سطح دیتابیس و به طور کلی در مورد ایندکس گذاری صحبت کردیم. در این بخش قصد داریم یکی از موارد بسیار مهم، یعنی ذخیره‌ی فایل‌های باینری را در دیتابیس، مورد بررسی قرار دهیم. روش‌های مختلفی برای اینکار وجود دارند؛ ولی بعضی از این روش‌ها در حال حاضر منسوخ شده اعلام شده‌اند که در اینجا ما آخرین روش را که در حال حاضر هیچ ویژگی منسوخ شده‌ای ندارد، به کار می‌گیریم.
از آنجاکه نهایت اندازه‌ی یک سند BSON نمی‌تواند بیشتر از 16 مگابایت باشد، قابلیتی به نام GridFS ایجاد شده‌است تا بتوان فایل‌های باینری را در آن ذخیره کرد. GridFS شامل دو بخش مختلف برای ذخیره اطلاعات یک فایل باینری است:
- fs.chunks که برای ذخیره اطلاعات قطعه‌های یک فایل باینری به کار میرود.
- fs.files که برای ذخیره اطلاعات و متادیتاها به کار می‌رود.

قبل از هر چیزی باید بدانید که کتابخانه مربوط به GridFs در یک پکیج جداگانه عرضه شده است و باید آن از طریق nuget نصب کنیم:
install-package MongoDB.Driver.GridFS

برای آپلود یک فایل باینری به داخل سیستم از کد زیر استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
ابتدا یک شیء GridFsBucket را ایجاد میکنیم که از ما اطلاعات دیتابیس مورد نظر را برای ارسال فایل میخواهد و نتیجه‌ی آن یک کلاس از جنس اینترفیس IGridFSBucket می‌باشد. این باکت یا سطل در واقع همانند کالکشن رفتار میکند.
byte[] source=File.ReadAllBytes(@"D:\Untitled.png");
var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = new BsonDocument
                {
                    { "CoverType", "Front" }, 
                    { "copyrighted", true }
                }
            };
در مرحله بعد فایل باینری را به صورت آرایه‌ای از بایت‌ها میخوانیم (البته حالت‌های مختلفی چون استریم را نیز پشتیبانی میکند). بعد از خواندن فایل، یک شیء از جنس کلاس GridFSUploadOptions را ایجاد و اطلاعات فایل آپلودی را مشخص می‌کنیم. به عنوان مثال اولین خصوصیتی که پر میکنیم خصوصیت تعیین سایز قطعات فایل باینری می‌باشد که به طور پیشفرض بر روی 64 مگابایت قرار گرفته است و عموما هم برای اکثر موارد، پاسخگوی نیاز‌ها است (در بخش بعدی مقالات، بیشتر این مورد را بررسی میکنیم).
مورد دومی که مقداردهی شده‌است، متادیتا‌ها هستند و این قابلیت را داریم که پرس و جوی خود را بر اساس آن‌ها نیز فیلتر کنیم. این خصوصیت مقدار دریافتی از جنس BsonDocument را دریافت میکند. ولی اگر شما برای فایل خود، کلاس اختصاصی برای متادیتاها در نظر گرفته‌اید میتوانید از یک Extension Method به نام ToBsonDocument استفاده کنید و شیء خود را به این نوع تبدیل کنید:
 var options = new GridFSUploadOptions
            {
                ChunkSizeBytes = 64512, // 63KB
                Metadata = metaData.ToBsonDocument()
            };


در نهایت آن را آپلود میکنیم:
 var id = bucket.UploadFromBytes("GoneWithTheWind", source, options);
پارامتر اول آن، نامی برای بسته آپلودی است. پارامتر دوم، خود فایل آپلودی میباشد و با پارمتر آخر هم تنظیماتی را که برای فایل مورد نظر توسط کلاس GridFSUploadOptions  تعیین کرده‌ایم، مشخص میکنیم. موقعیکه آپلود انجام شود، به ازای این کد، یک شناسه‌ی اختصاصی از جنس ObjectId را دریافت میکنیم که می‌توانیم آن را به یک خصوصیت در سند اصلی نسبت دهیم تا ارتباط بین سند و فایل هایش را داشته باشیم.

نکته: اگر در یک کلاس، چند فیلد از جنس ObjectId دارید، مونگو در بین تشخیص شناسه اصلی سند و شناسه تصاویر، با توجه به نام خصوصیت‌ها و غیره، تا حد زیادی هوشمند عمل میکند. ولی اگر خواستید صریحا شناسه اصلی را ذکر کنید و آن را متمایز از بقیه نشان دهید، می‌توانید از خصوصیت BsonId در بالای نام فیلد ID استفاده کنید:
[BsonId]
public ObjectId Id { get; set; }

جهت خواندن فایل آپلود شده، تنها کافی است از طریق شناسه‌ی دریافتی در مرحله‌ی آپلود، اقدام نماییم:
var bytes = bucket.DownloadAsBytes(id);

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


اگر قصد دارید بر اساس نام داده شده، فایل را دریافت کنید، ممکن است که چندین فایل، تحت یک نام ذخیره شده باشند که میتوانید در حالت‌های مختلفی این تصاویر را واکشی نمایید:
0 : فایل اصلی
1: اولین نسخه فایل
2: دومین نسخه فایل
و الی آخر...

1-: جدیدترین نسخه فایل (مقدار پیش فرض)
2-: نسخه ماقبل جدیدترین نسخه فایل
و الی آخر...


منظور از نسخه، فایل‌هایی با نامی موجود و از قبل ذخیره شده هستند که نسخه جدیدی از فایل قبلی بوده و فایل اول، فایل اصلی محسوب میشود.
برای درک بهتر مسئله، من تصاویر زیر را به ترتیب از سمت راست به سمت چپ، تحت یک نام، وارد سیستم میکنم:


اولین تصویر، تصویر اصلی محسوب می‌شود و بعد از آن، نسخه اول و نسخه دوم تصویر، وارد سیستم می‌شوند و تکه کد زیر از آنجاکه با مقدار پیش فرض پر شده‌است باید آخرین تصویر، تصویر سمت چپ را برای شما بر روی دیسک ذخیره کند:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var image=bucket.DownloadAsBytesByName("City of Glass-cover");
File.WriteAllBytes(@"D:\a.jpg",image);

برای مقداردهی خواص بالا به شکل زیر عمل میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var options = new GridFSDownloadByNameOptions
            {
                Revision = 0
            };
var image=bucket.DownloadAsBytesByName("City of Glass-cover",options);
File.WriteAllBytes(@"D:\a.jpg",image);
با پاس کردن مقدار 0 به مونگو، اولین تصویر وارد شده، یعنی تصویر اصلی را دریافت می‌کنیم که اولین تصویر از سمت راست می‌شود. اگر مقادیر 1 یا 1- را پاس دهیم، چون تنها سه تصویر بیشتر نیست، در هر دو حالت تصویر دوم بازگردانده می‌شود.

برای بازگردانی تصاویر از طریق مقادیر موجود در متادیتا، باید از کلاس ویژه‌ای به نام GridFSFileInfo استفاده کنیم. در اینجا هم همانند روزهای اول، از کلاس بیلدر جهت ایجاد شرط استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
IGridFSBucket bucket = new GridFSBucket(db);
var filter = Builders<GridFSFileInfo>.Filter.Gte(x => x.Length , 600);
var sort = Builders<GridFSFileInfo>.Sort.Descending(x => x.UploadDateTime);
var options =new GridFSFindOptions()
            {
                Limit = 3,
                Sort = sort
            };
var cursor = bucket.Find(filter, options);
var list = cursor.ToList();
در اینجا ابتدا مشخص کرده‌ایم که فایل مورد نظر باید حجمی بیشتر از 600 بایت داشته باشد و مرتب سازی آن به صورت نزولی، بر اساس زمان آپلود باشد. در عبارت لامبدا تعریف شده، می‌توانید خصوصیت‌های مختلف یک فایل از قبیل نام، حجم (سایز) ، زمان آپلود و ... را ببینید. سپس مرتب سازی و تعداد رکورد برگشتی از ابتدای جدول را مشخص میکنیم و از متد Find یا FindAsync جهت جست‌وجو استفاده میکنیم. با شکل گرفتن کوئری درخواست، لیستی از آن را تهیه میکنیم. مقدار بازگشتی این شیء، در واقع اسنادی از تصاویر هستند که میتوانید از طریق Id یا نام آن‌ها، فایل اصلی را واکشی نمایید.
برای یافتن تصاویر بر اساس متادیتاهای تعریف شده، از کد زیر استفاده میکنیم:
var filter = Builders<GridFSFileInfo>.Filter.Eq("metadata.CoverType","Front");

تغییر نام تصاویر
جهت ویرایش یک نام فایل از طریق متدهای زیر اقدام می‌نماییم:
bucket.Rename(id, newFilename);

//یا در حالت غیرهمزمان
await bucket.RenameAsync(id, newFilename);

حذف تصویر
bucket.Delete(id);

//یا
await bucket.DeleteAsync(id);

برای حذف کل bucket از طریق کد زیر اقدام می‌نماییم:
bucket.Drop();

//یا
await bucket.DropAsync();
 
در بخش بعدی Chunk را مورد بررسی قرار می‌دهیم.
مطالب
Blazor 5x - قسمت 27 - برنامه‌ی Blazor WASM - کار با سرویس‌های Web API
در قسمت‌های Blazor Server مثال این سری، با روش کار با سرویس‌های سمت سرور برنامه، آشنا شدیم. در این نوع برنامه‌ها، فقط کافی است اصل سرویس مدنظر را مستقیما در کامپوننت‌های Razor تزریق کرد و سپس می‌توان به نحو متداولی با آن‌ها کار کرد؛ اما در برنامه‌های Blazor WASM خیر! به این نوع برنامه‌های سمت کلاینت باید همانند برنامه‌های React ، Angular ، Vue و یا حتی برنامه‌های مبتنی بر jQuery نگاه کرد. در تمام فناوری‌های سمت کلاینت، این درخواست‌های Ajax ای هستند که با سرویس‌های یک Web API سمت سرور، ارتباط برقرار کرده، اطلاعاتی را به آن‌ها ارسال و یا دریافت می‌کنند. در برنامه‌های Blazor WASM نیز باید به همین ترتیب عمل کرد و در اینجا HttpClient دات نت، جایگزین برای مثال jQuery Ajax ، Fetch API و یا XMLHttpRequest استاندارد می‌شود (البته jQuery Ajax در اصل یک محصور کننده‌ی استاندارد XMLHttpRequest است که برای اولین بار توسط مایکروسافت در برنامه‌ی Outlook web access معرفی شد).


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

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

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

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

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

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

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

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

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

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

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

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

            // ...
        }
    }
}

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

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

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

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


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

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

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

            // ... 
        }
    }
}

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

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

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

<h3>HotelRooms</h3>

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

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

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


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

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

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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-27.zip
مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت چهارم - اعتبارسنجی Async سمت کلاینت و یا همان Remote Client Side Validation
در قسمت قبل با نحوه‌ی پیاده سازی اعتبارسنجی‌های سفارشی سمت کلاینت مخصوص کتابخانه‌ی Fluent Validation آشنا شدیم. در این قسمت، یک حالت خاص همان نوع اعتبارسنجی‌های سمت کلاینت را که remote validation نام دارد، بررسی می‌کنیم. در این حالت خاص، نیازی به کدنویسی جاوااسکریپتی خاصی نیست. چون زیرساخت آن به همراه unobtrusive jQuery Ajax خود ASP.NET Core ارائه می‌شود. در اینجا فقط نیاز است تا متادیتای خاص آن‌را تولید کنیم. به عبارتی اینبار هدف ما تنها تولید یک چنین تگ HTML ای است:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val ، data-val-remote و data-val-remote-url است. همینقدر که این سه ویژگی وجود داشته باشند، مابقی منطق اعتبارسنجی سمت کلاینت آن توسط unobtrusive jQuery Ajax (ارسال خودکار Ajax ای مقدار ایمیل، به سمت سرور و دریافت پاسخ) و unobtrusive java script validation مدیریت خواهند شد.


افزودن آدرس ایمیل به مدل کاربران

به همان مدل قسمت قبل، قصد داریم خاصیت آدرس ایمیل را هم اضافه کنیم:
using System.ComponentModel.DataAnnotations;

namespace FluentValidationSample.Models
{
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

        [Display(Name = "سن")]
        public int Age { get; set; }

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }

        [DataType(DataType.EmailAddress)]
        [Display(Name = "آدرس ایمیل")]
        public string Email { get; set; }
    }
}


ایجاد سرویسی برای بررسی منحصربفرد بودن آدرس ایمیل

در ادامه قصد داریم سرویسی را ایجاد کنیم که برای مثال با بانک اطلاعاتی ارتباط برقرار کرده (در اینجا جهت سهولت ارائه، از یک آرایه استفاده شده‌است) و مشخص می‌کند که آیا ایمیل دریافتی پیشتر استفاده شده‌است یا خیر:
using System.Linq;

namespace FluentValidationSample.Services
{
    public interface IUsersService
    {
        bool IsUniqueEmail(string emailAddress);
    }

    public class UsersService : IUsersService
    {
        public bool IsUniqueEmail(string emailAddress)
        {
            string[] registedEmails = {
                "email@site.com",
                "test@gmail.com"
            };
            return !registedEmails.Contains(emailAddress);
        }
    }
}
این سرویس را هم با طول عمر Scoped به برنامه معرفی می‌کنیم؛ چون طول عمر سرویس‌هایی که با بانک اطلاعاتی و DbContext کار می‌کنند، به همین نحو تعیین می‌شوند:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();


ایجاد اعتبارسنج سمت سرور بررسی منحصربفرد بودن آدرس ایمیل

در ادامه یک PropertyValidator جدید را ایجاد می‌کنیم تا بتوان توسط آن مقدار ایمیل دریافتی را در سمت سرور، تعیین اعتبار کرد:
using FluentValidation.Validators;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UniqueEmailValidator : PropertyValidator
    {
        private readonly IUsersService _usersService;

        public UniqueEmailValidator(IUsersService usersService)
                : base("این آدرس ایمیل هم اکنون مورد استفاده‌است")
        {
            _usersService = usersService;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            return context.PropertyValue != null &&
                    _usersService.IsUniqueEmail((string)context.PropertyValue);
        }
    }
}
همانطور که مشاهده می‌کنید، در اینجا تزریق سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج، مجاز است و بدون مشکل کار می‌کند.
پس از آن جهت سهولت استفاده‌ی از آن، یک متد الحاقی جدید را نیز به نام UniqueEmail به نحو زیر تعریف می‌کنیم:
using FluentValidation;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public static class CustomValidatorExtensions
    {
        public static IRuleBuilderOptions<T, string> UniqueEmail<T>(
            this IRuleBuilder<T, string> ruleBuilder, IUsersService usersService)
        {
            return ruleBuilder.SetValidator(new UniqueEmailValidator(usersService));
        }
    }
}

یک نکته: اگر دقت کرده باشید، فضای نام این اعتبارسنج در این قسمت FluentValidationSample.ModelsValidations شده‌است:


علت اینجا است که اعتبارسنج تعریف شده نیاز دارد هم از مدل‌ها استفاده کند و هم از سرویس کاربران. سرویس کاربران هم از مدل‌ها استفاده می‌کند. به همین جهت اگر تعاریف اعتبارسنجی را داخل پروژه‌ی مدل‌ها قرار دهیم، یک وابستگی حلقوی رخ خواهد داد (وابستگی مدل‌ها به سرویس‌ها و برعکس). بنابراین بهتر است اعتبارسنج‌ها را به یک پروژه‌ی مجزا منتقل کنیم تا از بروز این cyclic dependency جلوگیری شود.


اعمال اعتبارسنجی منحصربفرد بودن ایمیل دریافتی به اعتبارسنج UserModel

پس از تهیه‌ی متد الحاقی UniqueEmail، آن‌را به RuleFor مخصوص خاصیت ایمیل اضافه می‌کنیم. در اینجا نیز تزریق وابستگی سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج مجاز است:
using FluentValidation;
using FluentValidationSample.Models;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UserModelValidator : AbstractValidator<UserModel>
    {
        public UserModelValidator(IUsersService usersService)
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
            RuleFor(x => x.Email).EmailAddress().NotNull().UniqueEmail(usersService);
        }
    }
}


ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.ModelsValidations
{
    public class RemoteClientValidator : ClientValidatorBase
    {
        public string RemoteUrl { set; get; }

        public RemoteClientValidator(PropertyRule rule, IPropertyValidator validator) :
            base(rule, validator)
        {
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-remote", GetErrorMessage(context));
            MergeAttribute(context.Attributes, "data-val-remote-url", RemoteUrl);
        }

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر تگ HTML ای زیر می‌شود:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val، data-val-remote و data-val-remote-url است تا unobtrusive jQuery Ajax validation را فعال کند.


افزودن اعتبارسنج‌های تعریف شده به تنظیمات برنامه

پس از تعریف UniqueEmailValidator و RemoteClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();

            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        // ...

                        clientSideValidation.Add(
                            validatorType: typeof(UniqueEmailValidator),
                            factory: (context, rule, validator) =>
                                        new RemoteClientValidator(rule, validator)
                                        {
                                            RemoteUrl = "/Home/ValidateUniqueEmail"
                                        });
                    });
                }
            );
        }
در اینجا یک RemoteUrl را هم مشاهده می‌کنید که به صورت زیر باید تعریف شود:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUsersService _usersService;

        public HomeController(IUsersService usersService)
        {
            _usersService = usersService;
        }

         // ...

        public IActionResult ValidateUniqueEmail(string email)
        {
            return Ok(_usersService.IsUniqueEmail(email));
        }
    }
}
زمانیکه اعتبارسنجی سمت کلاینت رخ می‌دهد، آدرس ایمیل، به اکشن متد فوق ارسال شده و یک true و یا false را دریافت می‌کند که بیانگر موفقیت آمیز بودن و یا شکست اعتبارسنجی از راه دور است.


تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه نیازی به تکمیل فایل js/site.js جهت افزودن کدهای remote client validation نیست و این کدها جزئی از کتابخانه‌ی jquery.validate.unobtrusive هستند.


آزمایش برنامه

View این قسمت نیز همانند قسمت قبل است که فقط یک آدرس ایمیل به آن اضافه شده‌است:


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


کدهای کامل این سری را تا این قسمت از اینجا می‌توانید دریافت کنید: FluentValidationSample-part04.zip
بازخوردهای دوره
مدیریت نگاشت ConnectionIdها در SignalR به کاربران واقعی سیستم
سلام
من میخواهم برای هر چت یک کانکشن آی دی در نظر بگیریم نه به ازای هر کاربر به این صورت در یک صفحه درخواست چت داده میشه و کاربر با کلیک بر روی درخواست یک چت مجزا باز میشه وی یک کانکشن ای دی برای چت در نظر گرفته میشه و در جدل chat  ذخیره میشه و پیام‌ها بر اساس این کانکشن  ای دی در جدولی  دیگر به نام chatlogs ذخیره میشه تا بعدا بتونیم پیام‌های رد وبدل شده در این چت رو بازیابی کنیم 
مشکل اینجاست در متد جاوا اسکریپت صفحه ای که چت رو ایجاد میکنه هنگام پاس دادن connection.hub.start.$ 
کانکشن ای دی رو ایجاد میکنه ولی در صفحه چت وقتی میخوای پیام‌ها رو به متد ثبت چت بدی مجبوری دوباره 
connection.hub.start.$ کنم واین میاد هاب رو از اول اجرا میکنه که به معنی نال کردن مجدد متغییر‌های گلوبال ایا راهی وجود داره
Chat c = new Chat();
            ChatLog m = new ChatLog();
        public Guid NewObjects(Guid Id)
        {
    
            return Id;
      
         }
        public Guid idChat { get ; set; }
        public void CreateChat()
        {
  
           Guid Id = Guid.Parse(Context.ConnectionId);
             NewObjects(Id);
            idchat=Id;
            c.ChatId = Id;
            c.Time = 25;
            c.UserId = "87EC3AD1 - 53D1 - 4649 - 8CF3 - 2CD5ADB1938C";
         
            var chat = db.Chats.FirstOrDefault(c => c.ChatId == Id);
            if (chat == null)
            {
                db.Chats.Add(c);
                db.SaveChanges();
            }
            db.Dispose();
     

        }

      
        public void broadcastMessage(string name, string message, Guid Id)
        {


           
            m.ChatText = message;
            m.Id = Id;
            m.ChatDate = DateTime.Now;
            m.UserId = "87EC3AD1 - 53D1 - 4649 - 8CF3 - 2CD5ADB1938C";
            db.ChatLogs.Add(m);
            db.SaveChanges();

            db.Dispose();
            Clients.All.Msg(name, message,m.Id);


        }
خیلی گشتم ولی متدهای هاب جاوا اسکریپت نیست تا بتونم راهی پیدا کنم
انگار مایکروسافت از روی قصد اینکارو کرده
نظرات مطالب
فعال سازی قسمت ارسال فایل و تصویر ویرایشگر آنلاین RedActor در ASP.NET MVC
ممنون از پاسختون
من از ورژن v8.0.3 استفاده میکنم 
میشه لطفاً بیشتر توضیح بدید که مثلاً چه جوری از فایل paste_code.html باید استفاده کرد یا اینکه  code:
{
    title: 'Code',
    func: 'showCodesPage'
},
رو باید کجا اضافه کرد؟ تو سایت خودش واسه نوشتن پلاگین از یه روش دیگه استفاده کرده
مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت سوم - گردش کاری‌های متداول
در قسمت قبل، دو عمل متداول نحوه‌ی ایجاد و اجرای یک پروژه‌ی جدید ASP.NET Core را بررسی کردیم. در ادامه می‌خواهیم معادل سایر اعمالی را که می‌توان با نگارش کامل ویژوال استودیو انجام داد، در VSCode نیز برشماریم.


کار با IDE و حرکت بین کدها

در ادامه‌، همان پروژه‌ای را که در قسمت قبل ایجاد کردیم، مجددا با وارد شدن به پوشه‌ی آن و اجرای دستور . code، توسط VSCode باز خواهیم کرد. سپس فایل Program.cs آن‌را باز کنید. فرض کنید در سطر ذیل آن:
 .UseStartup<Startup>()
می‌خواهیم به نحو ساده‌تری به کلاس Startup آن مراجعه کنیم. برای اینکار دو روش وجود دارد:
الف) اشاره‌گر ماوس را به آن نزدیک کنید و سپس دکمه‌ی Ctrl را نگه دارید. به این ترتیب واژه‌ی Startup تبدیل به یک لینک خواهد شد که با کلیک بر روی آن می‌توان به کلاس Startup رسید.
ب) روش دوم، قرار دادن اشاره‌گر متنی بر روی واژه Startup و سپس فشردن دکمه‌ی F12 است. این گزینه بر روی منوی کلیک راست بر روی این واژه نیز وجود دارد.

اگر دکمه‌ی F12 را بر روی کلاسی فشار دهید که کدهای آن در IDE وجود ندارند، یک صفحه‌ی جدید باز شده و کلاس تعریف آن‌را بر اساس ساختار و متادیتای این اسمبلی، به همراه مستندات کامل آن‌ها نمایش می‌دهد.

در این حالت اگر خواستید به مکان قبلی بازگردید فقط کافی است دکمه‌های alt + left cursor key را بفشارید.


در اینجا امکان یافتن ارجاعات به یک کلاس، مشاهده‌ی پیاده سازی‌ها و همچنین یک Refactoring به نام rename symbol نیز موجود است که با استفاده‌ی از آن، تمام ارجاعات به این کلاس در IDE نیز تغییرنام خواهند یافت.


یافتن سریع فایل‌ها در IDE

در یک پروژه‌ی بزرگ، برای یافتن سریع یک فایل، تنها کافی است دکمه‌های Ctrl+P را فشرده و در صفحه‌ی دیالوگ باز شده، قسمتی از نام آن‌را جستجو کنید:


همچنین اگر می‌خواهید محتوای فایل‌ها را جهت یافتن واژه‌ای خاص جستجو کنید، کلیدهای Ctrl+Shift+F (منوی Edit بالای صفحه و یا دومین آیکن در نوار ابزار عمودی سمت چپ صفحه) چنین امکانی را فراهم می‌کنند:



افزودن فایل‌های جدید به پروژه

فرض کنید می‌خواهیم یک کنترلر جدید را به پوشه‌ی Controllers اضافه کنیم:


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

در ادامه فایل جدید AboutController.cs را در پوشه‌ی Controllers ایجاد کنید. مشاهده خواهید کرد که یک فایل کاملا خالی ایجاد شده‌است. در VSCode به ازای پسوندهای مختلف فایل‌ها، قالب‌های از پیش آماده شده‌ای برای آن‌ها درنظر گرفته نمی‌شود و نمای ابتدایی تمام آن‌ها خالی است.
اما در اینجا اگر کلمه‌ی name را تایپ کنیم، دو پیشنهاد افزودن فضای نام را ارائه می‌دهد:


اولی صرفا نام namespace را در صفحه درج خواهد کرد. دومی به یک code snippet اشاره می‌کند و کار آن ایجاد قالب یک فضای نام جدید است. برای درج آن فقط کافی است دکمه‌ی tab را بفشارید.
همین کار را در مورد class نیز می‌توان تکرار کرد و در اینجا در intellisense ظاهر شده یا می‌توان واژه‌ی class را درج کرد و یا code snippet آن‌را انتخاب نمود که یک کلاس جدید را ایجاد می‌کند:


یک نکته: در VSCode نیازی نیست تا مدام دکمه‌های Ctrl+S را جهت ذخیره سازی فایل‌های تغییر کرده فشرد. می‌توان از منوی فایل، گزینه‌ی Auto Save را انتخاب کرد تا این‌کار را به صورت خودکار انجام دهد.


ایجاد Code Snippets جدید

هرچند تا اینجا با استفاده از code snippets پیش فرض فضاهای نام و کلاس‌ها، یک کلاس جدید را ایجاد کردیم، اما روش ساده‌تری نیز برای انجام این‌کارها و تکمیل کنترلر وجود دارد.
برای این منظور به منوی File -> Preferences -> User snippets مراجعه کنید و سپس از لیست ظاهر شده، زبان #C را انتخاب کنید:


به این ترتیب یک قالب جدید code snippet تولید خواهد شد. در اینجا می‌خواهیم برای تولید Actionهای یک کنترلر نیز یک code snippet جدید را تهیه کنیم:
{
    "MVC Action": {
        "prefix": "mvcAction",
        "body": [
            "public IActionResult ${1:ActionName}()",
            "{",
            " $0 ",
            " return View();",
            "}"
        ],
        "description": "Creates a simple MVC action method."
    }
}
- کدهای snippet، در داخل آرایه‌ی body درج می‌شوند. هر عضو این آرایه یک سطر از کدها را تشکیل خواهد داد.
- در این آرایه، 0$ جائی است که اشاره‌گر متنی پس از درج snippet قرار می‌گیرد و 1$ به نامی که قرار است توسط کاربر تکمیل شود، اشاره می‌کند که در اینجا یک نام پیش فرض مانند ActionName را هم می‌توان برای آن درنظر گرفت.
- در اینجا prefix نامی است که اگر در صفحه تایپ شود، منوی انتخاب این code snippet را ظاهر می‌کند:



استفاده از بسته‌های Code Snippets آماده

خوشبختانه پشتیبانی جامعه‌ی توسعه دهندگان از VSCode بسیار مطلوب است و علاوه بر افزونه‌های قابل توجهی که برای آن نوشته شده‌اند، بسته‌های Code Snippets آماده‌ای نیز جهت بالابردن سرعت کار با آن وجود دارند. برای دریافت آن‌ها، به نوار ابزار عمودی که در سمت چپ صفحه وجود دارد، مراجعه کنید و گزینه‌ی extensions آن‌را انتخاب نمائید:


در اینجا اگر aspnetcore را جستجو کنید، لیست بسته‌های code snippets برچسب گذاری شده‌ی با aspnetcore ظاهر می‌شود. در همینجا اگر یکی از آن‌ها را انتخاب کنید، در سمت راست صفحه می‌توانید توضیحات آن را نیز مشاهده و مطالعه نمائید (بدون نیاز به خروج از IDE).
برای مثال wilderminds-aspnetcore-snippets را نصب کنید. پس از آن تنها کافی است mvc6 را درون یک کلاس کنترلر تایپ نمائید تا امکانات آن ظاهر شوند:


برای نمونه پس از ایجاد یک فایل خالی کنترلر، انتخاب code snippet ایی به نام mvc6-controller، سبب ایجاد یک کنترلر کامل به همراه اکشن متدهایی پیش فرض می‌شود. بنابراین معادل قالب‌های new items در نگارش کامل ویژوال استودیو در اینجا می‌توان از Code Snippets استفاده کرد.

درکل برای یافتن مواردی مشابه، بهتر است واژه‌ی کلیدی snippets را در قسمت extensions جستجو نمائید و آن‌ها صرفا مختص به #C یا ASP.NET Core نیستند.


مدیریت ارجاعات و بسته‌های نیوگت در برنامه‌های ASP.NET Core

تا اینجا مدیریت فایل‌ها و نحوه‌ی تکمیل آن‌ها را توسط code snippets، بررسی کردیم. قدم بعدی و گردش کاری مهم مورد نیاز دیگر، نحوه‌ی افزودن ارجاعات به پروژه در VSCode است.
مدیریت ارجاعات در نگارش‌های جدید ASP.NET Core، در فایل csproj برنامه انجام می‌شوند. در اینجا است که بسته‌های نیوگت جدید، ارجاعات به پروژه‌های دیگر و حتی شماره فریم ورک مورد استفاده تعیین می‌شوند.
برای نمونه بسته‌ی نیوگت DNTPersianUtils.Core را به لیست ارجاعات آن اضافه کنید:
 <PackageReference Include="DNTPersianUtils.Core" Version="2.2.0" />
بلافاصله با انجام این تغییر و ذخیره‌ی فایل csproj، گزینه‌ی بازیابی و نصب آن نیز ظاهر می‌شود:


در اینجا با کلیک بر روی لینک و یا دکمه‌ی restore ، کار دریافت این بسته و نصب آن انجام خواهد شد.
اگر علاقمند بودید تا اینکار را در خط فرمان به صورت دستی انجام دهید، دکمه‌های Ctrl+ back-tick را فشرده، تا امکانات خط فرمان درون VSCode ظاهر شوند و سپس دستور dotnet restore را صادر کنید.

روش دوم ثبت بسته‌های نیوگت در فایل csproj، مراجعه به خط فرمان (فشردن دکمه‌های دکمه‌های Ctrl+ back-tick) و صدور دستور ذیل است:
 > dotnet add package DNTPersianUtils.Core
به این ترتیب با استفاده از امکانات dotnet-cli نیز می‌توان آخرین نگارش یک بسته‌ی نیوگت را دریافت و به صورت خودکار به پروژه اضافه نمود. اگر نگارش خاصی مدنظر است، باید توسط پرچم version-- آن‌را در انتهای دستور مشخص کرد.


دیباگ برنامه‌های ASP.NET Core در VSCode

در قسمت قبل با فرامین dotnet run و dotnet build و همچنین نحوه‌ی اجرای سریع آن‌ها آشنا شدیم. در ادامه اگر به نوار ابزار عمودی کنار صفحه‌ی آن دقت کنید، گزینه‌ی دیباگ نیز وجود دارد:


در اینجا دو نوع نحوه‌ی برپایی و اجرای برنامه را مشاهده می‌کنید که هر دو مورد در فایل vscode\launch.json. زمانیکه دیباگ پروژه را در ابتدای باز کردن آن در VSCode فعال می‌کنیم، تعریف شده‌اند.
برای بررسی آن فایل HomeController.cs را گشوده و در ابتدای متد About آن یک break point را قرار دهید (با حرکت دادن اشاره‌گر ماوس، جائیکه شماره سطرها قرار دارند، علامت درج break point ظاهر می‌شود که با کلیک بعدی، دائمی خواهد شد و برعکس):


پس از آن تنها کاری که باید انجام داد، فشردن دکمه‌ی F5 است که به معنای اجرای برنامه به همراه اتصال دیباگر به آن می‌باشد (در قسمت قبل، Ctrl+F5 را بررسی کردیم که به معنای اجرای برنامه، بدون اتصال دیباگر به آن است).
در ادامه پس از اجرای برنامه، بر روی لینک About کلیک کنید تا اکشن متد آن اجرا شود. بلافاصله کنترل کار به VSCode بازگشته و سطری که بر روی آن break-point قرار داده بودیم، ظاهر می‌شود:


اطلاعات بیشتر آن در برگه‌ی دیباگ ظاهر می‌شوند و در کل تجربه‌ی کاربری آن همانند سایر IDEهایی است که تاکنون با آن‌ها کار کرده‌اید.

یک نکته: در اینجا در داخل فایل‌های View (فایل‌های razor) نیز می‌توان break point قرار داد.


برپایی یک Watcher Build

ابزار ویژه‌ای به همراه ابزارهای Build مخصوص پروژه‌های NET Core. وجود دارد به نام watcher که تغییرات پوشه‌های برنامه را تحت نظر قرار داده و با هر تغییری، پروژه را کامپایل می‌کند. به این ترتیب به سرعت می‌توان آخرین تغییرات برنامه را در مرورگر بررسی کرد. برای نصب آن، تنظیم ذیل را به فایل csproj برنامه اضافه کنید:
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="1.0.0" />
  </ItemGroup>
پس از آن دکمه‌های Ctrl+ back-tick را فشرده تا امکانات خط فرمان، درون VSCode ظاهر شود و سپس دستور dotnet restore را صادر کنید.

اکنون برای استفاده‌ی از آن تنها کافی است دستور ذیل را صادر کنید:
 >dotnet watch run
?[90mwatch : ?[39mStarted
Hosting environment: Production
Content root path: D:\vs-code-examples\FirstAspNetCoreProject
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
همانطور که مشاهده می‌کنید، پروژه را کامپایل کرده و بر روی پورت 5000 ارائه می‌دهد. به علاوه از این پس با هر تغییری در فایل‌های #C پروژه، این کامپایل خودکار بوده و نیازی به تکرار این عملیات نیست.

یک نکته: اینبار برای دیباگ برنامه، باید گزینه‌ی attach را انتخاب کرد:


اگر بر روی دکمه‌ی سبز رنگ کنار آن کلیک کنید، لیست پروسه‌های دات نتی ظاهر شده و در این حالت می‌توانید دیباگر را به پروسه‌ی dotnet exec ایی که به dll برنامه اشاره می‌کند، متصل کنید (و نه پروسه‌ی dotnet watch run که در حقیقت پروسه‌ی dotnet exec را مدیریت می‌کند).
نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت سوم
من اگر بخوام یک لیست در صفحه‌ی اصلی داشته باشم که آخرین مطالب سایت رو نشون بده، که شامل آخرین مطالب هر پلاگین مانند اخبار و مقالات و ... باشه، چطور میتونم اطلاعات رو از پلاگین‌ها بگیرم تا اصول پلاگین بیس به هم نریزه؟
آیا میشه توی این کلاس NewsPlugin، مانند این کد که با GetMenuItem   آدرس پلاگین رو بدست آوردیم، بتونیم به بانک متصل شده و لیست اطلاعات پلاگین رو هم داشته باشیم!    
@using MvcPluginMasterApp.IoCConfig
@using MvcPluginMasterApp.PluginsBase
@{
    var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
}

@foreach (var plugin in plugins)
{
    var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
    <li>
        <a href="@menuItem.Url">@menuItem.Name</a>
    </li>
}