مطالب
Closure در JavaScript
در قسمت قبلی درباره علت نیاز به الگوهای طراحی در JavaScript و Function Spaghetti code صحبت شد. در این قسمت Closure در JavaScript مورد بررسی قرار می‌گیرد. 
در JavaScript می‌توان توابع تو در تو نوشت (nested functions) ، زمانی که یک تابع درون تابع دیگر تعریف می‌شود تابع درونی به تمام متغیر‌ها و توابع تابع بیرونی (Parent) دسترسی دارد.
Douglas Crockford برای تعریف Closure می‌گوید : 

an inner function always has access to the vars and parameters of its outer function, even after the outer  function has returned

یک تابع درونی (nested) همیشه به متغیر‌ها و پارامتر‌ها تابع بیرونی دسترسی دارد ، حتی اگر تابع بیرونی مقدار برگردانده باشد. 

تابع زیر را در نظر بگیرید : 
// The getDate() function returns a nested function which refers the 'date' variable defined
// by outer function getDate()
function getDate() {
   var date = new Date();    // This variable stays around even after function returns

   // nested function
   return function () {
      return date.getMilliseconds();
   }
}

  اکنون اگر به صورت زیر تابع getDate فراخوانی شود مشاهده می‌شود که تابع درونی (با کامنت nested function مشخص شده است.) به شیء date دسترسی دارد.
// Once getDate() is executed the variable date should be out of scope and it is, but since
// the inner function
// referenes date, this value is available to the inner function.
var dt = getDate();

alert(dt());
alert(dt());
خروجی هر 2 alert یک مقدار خواهد بود. 
اگر از فردی که به تازگی رو به JavaScript آورده است خواسته شود تابعی بنویسد که میلی ثانیه‌ی زمان جاری را برگداند احتمالا همچین کدی تحویل می‌دهد : 
        function myNonClosure() {
            var date = new Date();
            return date.getMilliseconds();
        }
در کد بالا پس از اجرای myNonClosure متغیر date از بین خواهد رفت ، این مسئله در دنیای JavaScript طبیعی هست.
این مثال را در نظر بگیرید :  
var MyDate = function () {
    var date = new Date();
    var getMilliSeconds = function () {
        return date.getMilliseconds();
    }
}

var dt = new MyDate();

alert(dt.getMilliSeconds());  // This will throw error as getMilliSeconds is not accessible.
در صورت اجرای مثال بالا خطایی با این مضمون دریافت خواهد شد که getMilliSeconds دستیابی پذیر نیست. (کپسوله شده)  برای اینکه آن را دستیابی پذیر کنیم کد را به این صورت تغییر می‌دهیم :
// This is closure 
var MyDate = function () {
    var date = new Date();   // variable stays around even after function returns
    var getMilliSeconds = function () {
        return date.getMilliseconds();
    };
    return {
        getMs : getMilliSeconds
    }
}
آنچه در تابع بالا انجام شده کپسوله سازی همه‌ی منطق کار (منطق کار در اینجا برگرداندن میلی ثانیه زمان جاری می‌باشد) در یک فضای نام به نام MyDate می‌باشد. همچنین فقط متد‌های عمومی در اختیار استفاده کننده این تابع قرار داده شده است. برای استفاده می‌توان بدین صورت عمل کرد : 
var dt = new MyDate();
alert(dt.getMs());  // This should work.
در کد بالا برای توابع و متغیر‌های درونی یک container ایجاد کردیم که باعث جلوگیری از تداخل در نام متغیر‌ها با دیگر کد‌ها خواهد شد . (برای مشاهده‌ی تداخل‌ها به قسمت قبلی  توجه کنید.)
اگر بخواهیم Closure را تشبیه کنیم ، Closure شبیه به کلاس‌ها در C# یا Java هست. 
Closure یک حوزه (scope) برای متغیر‌ها و توابع درونی خودش ایجاد می‌کند.
jQuery بهترین مثال کاربردی برای Closure می‌باشد : 
(function($) {

    // $() is available here

})(jQuery);
در ادامه این مفاهیم بیشتر توضیح داده می‌شودند ، اکنون می‌خواهیم مشکلی که در قسمت قبلی مطرح کردیم به کمک Closure حل کنیم : 
 در آن مثال گفته شد که اگر : 
// file1.js
function saveState(obj) {
    // write code here to saveState of some object
    alert('file1 saveState');
}
// file2.js (remote team or some third party scripts)
function saveState(obj, obj2) {
     // further code...
    alert('file2 saveState");
}
اگر تابعی به نام saveState در 2 فایل مختلف داشته باشیم و این 2 فایل را بدین صورت در برنامه آدرس دهیم : 
<script src="file1.js" type="text/javascript"></script>
<script src="file2.js" type="text/javascript"></script>
تابع saveState در فایل دوم تابع saveState فایل اول را override می‌کند. یک از توابع بالا را به صورت زیر باز نویسی می‌کنیم و منطق کار را کپسوله می‌کنیم : 
function App() {
    var save = function (o) {
        // write code to save state here..
        // you have acces to 'o' here...
        alert(o);
    };

    return {
        saveState: save
    };
}
بدون نگرانی تداخل saveState با بقیه saveState‌ها در هر پلاگین یا فایل دیگری می‌توان از saveState می‌توان اینگونه استفاده کرد : 
var app = new App();

app.saveState({ name: "rajesh"});
برای اطلاعات بیشتر در مورد Closure ها این لینک  را بررسی کنید.
مطالب
تنظیمات و نکات کاربردی کتابخانه‌ی JSON.NET
پس از بررسی مقدماتی امکانات کتابخانه‌ی JSON.NET، در ادامه به تعدادی از تنظیمات کاربردی آن با ذکر مثال‌هایی خواهیم پرداخت.


گرفتن خروجی CamelCase از JSON.NET

یک سری از کتابخانه‌های جاوا اسکریپتی سمت کلاینت، به نام‌های خواص CamelCase نیاز دارند و حالت پیش فرض اصول نامگذاری خواص در دات نت عکس آن است. برای مثال بجای UserName به userName نیاز دارند تا بتوانند صحیح کار کنند.
روش اول حل این مشکل، استفاده از ویژگی JsonProperty بر روی تک تک خواص و مشخص کردن نام‌های مورد نیاز کتابخانه‌ی جاوا اسکریپتی به صورت صریح است.
روش دوم، استفاده از تنظیمات ContractResolver می‌باشد که با تنظیم آن به CamelCasePropertyNamesContractResolver به صورت خودکار به تمامی خواص به صورت یکسانی اعمال می‌گردد:
var json = JsonConvert.SerializeObject(obj, new JsonSerializerSettings
{
   ContractResolver = new CamelCasePropertyNamesContractResolver()
});


درج نام‌های المان‌های یک Enum در خروجی JSON

اگر یکی از عناصر در حال تبدیل به JSON، از نوع enum باشد، به صورت پیش فرض مقدار عددی آن در JSON نهایی درج می‌گردد:
using Newtonsoft.Json;

namespace JsonNetTests
{
    public enum Color
    {
        Red,
        Green,
        Blue,
        White
    }

    public class Item
    {
        public string Name { set; get; }
        public Color Color { set; get; }
    }

    public class EnumTests
    {
        public string GetJson()
        {
            var item = new Item
            {
                Name = "Item 1",
                Color = Color.Blue 
            };

            return JsonConvert.SerializeObject(item, Formatting.Indented);
        }
    }
}
با این خروجی:
{
  "Name": "Item 1",
  "Color": 2
}
اگر علاقمند هستید که بجای عدد 2، دقیقا مقدار Blue در خروجی JSON درج گردد، می‌توان به یکی از دو روش ذیل عمل کرد:
الف) مزین کردن خاصیت از نوع enum به ویژگی JsonConverter از نوع StringEnumConverter:
  [JsonConverter(typeof(StringEnumConverter))]
  public Color Color { set; get; }
ب) و یا اگر می‌خواهید این تنظیم به تمام خواص از نوع enum به صورت یکسانی اعمال شود، می‌توان نوشت:
return JsonConvert.SerializeObject(item, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   Converters = { new StringEnumConverter() }
});


تهیه خروجی JSON از مدل‌های مرتبط، بدون Stack overflow

دو کلاس گروه‌های محصولات و محصولات ذیل را درنظر بگیرید:
   public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Product> Products { get; set; }

        public Category()
        {
            Products = new List<Product>();
        }
    }

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual Category Category { get; set; }
    }
این نوع طراحی در Entity framework بسیار مرسوم است. در اینجا طرف‌های دیگر یک رابطه، توسط خاصیتی virtual معرفی می‌شوند که به آن‌ها خواص راهبری یا navigation properties هم می‌گویند.
با توجه به این دو کلاس، سعی کنید مثال ذیل را اجرا کرده و از آن، خروجی JSON تهیه کنید:
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace JsonNetTests
{
    public class SelfReferencingLoops
    {
        public string GetJson()
        {
            var category = new Category
            {
                Id = 1,
                Name = "Category 1"
            };
            var product = new Product
            {
                Id = 1,
                Name = "Product 1"
            };

            category.Products.Add(product);
            product.Category = category;

            return JsonConvert.SerializeObject(category, new JsonSerializerSettings
            {
                Formatting = Formatting.Indented,
                Converters = { new StringEnumConverter() }
            });
        }
    }
}
برنامه با این استثناء متوقف می‌شود:
 An unhandled exception of type 'Newtonsoft.Json.JsonSerializationException' occurred in Newtonsoft.Json.dll
Additional information: Self referencing loop detected for property 'Category' with type 'JsonNetTests.Category'. Path 'Products[0]'.
اصل خطای معروف فوق «Self referencing loop detected» است. در اینجا کلاس‌هایی که به یکدیگر ارجاع می‌دهند، در حین عملیات Serialization سبب بروز یک حلقه‌ی بازگشتی بی‌نهایت شده و در آخر، برنامه با خطای stack overflow خاتمه می‌یابد.

راه حل اول:
به تنظیمات JSON.NET، مقدار ReferenceLoopHandling = ReferenceLoopHandling.Ignore را اضافه کنید تا از حلقه‌ی بازگشتی بی‌پایان جلوگیری شود:
return JsonConvert.SerializeObject(category, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
   Converters = { new StringEnumConverter() }
});
راه حل دوم:
به تنظیمات JSON.NET، مقدار PreserveReferencesHandling = PreserveReferencesHandling.Objects را اضافه کنید تا مدیریت ارجاعات اشیاء توسط خود JSON.NET انجام شود:
return JsonConvert.SerializeObject(category, new JsonSerializerSettings
{
   Formatting = Formatting.Indented,
   PreserveReferencesHandling = PreserveReferencesHandling.Objects,
   Converters = { new StringEnumConverter() }
});
خروجی حالت دوم به این شکل است:
{
  "$id": "1",
  "Id": 1,
  "Name": "Category 1",
  "Products": [
    {
      "$id": "2",
      "Id": 1,
      "Name": "Product 1",
      "Category": {
        "$ref": "1"
      }
    }
  ]
}
همانطور که ملاحظه می‌کنید، دو خاصیت $id و $ref توسط JSON.NET به خروجی JSON اضافه شده‌است تا توسط آن بتواند ارجاعات و نمونه‌های اشیاء را تشخیص دهد.
مطالب
نحوه‌ی استخراج شماره سریال سخت افزار برای تولید یک قفل سخت افزاری
شما در حال نوشتن یک نرم افزار هستید و برای این نرم افزار ممکن است ماه‌ها وقت صرف کرده باشید؛ پس باید به دنبال راهی باشید که بتوانید از آن محافظت کنید. راه‌های متعددی برای Trial کردن نرم افزار وجود دارند که یکی از این راه‌ها استفاده از سریال سخت افزارهای کامپیوتر کاربر است. همانطور که می‌دانید هر سخت افزار یک شماره‌ی سریال مخصوص خودش را دارد و بدین طریق میتوان یک شماره سریال منحصر به فرد را تولید کرد. ما در این مقاله برای بدست آوردن کلیه‌ی مشخصات سخت افزار یک کامپیوتر از کلاس ManagementObjectSearcher در فضای نام System.Management استفاده کرده‌ایم. 
using System.Management;
using System.Windows.Forms;
namespace HardwareSerialNumber
{
    class Program
    {
        static void Main()
        {
            string serialNumber = string.Empty;
            ManagementObjectSearcher searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_Processor");
            foreach (var o in searcher.Get())
            {
                var query = (ManagementObject)o;

                serialNumber = serialNumber + query["ProcessorId"];
            }
            MessageBox.Show(string.Format("Processor Serial Number :{0}", serialNumber),"Serial Number",MessageBoxButtons.OK,MessageBoxIcon.Exclamation);

        }
    }
}
خوب. حالا به بررسی کدهای بالا می‌پردازیم. در خط 10 یک شیء از کلاس MangementObjectSearcher ایجاد کرده‌ایم . سازنده‌ی این کلاس 2 پارامتر را می‌پذیرد. اولین پارامتر یک رشته می‌باشد CIMV2 . root\\CIMV2 فضای نام پیش فرض مخصوص کوئری‌های WMI می‌باشد و پارامتر دوم کوئری WMI است. عبارت Win32_Processor اشاره به کلاس Win32_processor دارد. این کلاس بیانگر یک کلاس مدیریتی از نوع CIM (مراجعه کنید به اینجا و اینجا ) است. در خط 11 با استفاده از متد Get، تمامی اطلاعات مربوط به Processor را بارگذاری می‌کنیم و سپس فیلد ProcessorId را در یک رشته قرار می‌دهیم. نکته‌ی دیگر اینکه ارجاعی را به فایل System.Managment.Dll به پروژه‌ی خود اضافه کنید.
نتیجه‌ی کد بالا : 

در این آدرس یک کتابخانه که شامل تمامی مثالها می‌باشد قابل در یافت است.

مطالب
نحوه ایجاد فیلدهای محاسباتی در PdfReport
در گزارشات، گاهی از اوقات نیاز خواهد شد تا تعدادی ستون جدید را بر اساس مقادیر فیلدهای موجود در منبع داده گزارش، به صورت پویا محاسبه و تولید کنیم (ایجاد ستون‌هایی که در منبع داده وجود خارجی ندارند). در ادامه با نحوه انجام اینکار در PdfReport آشنا خواهیم شد.
در ابتدا کدهای کامل گزارش این قسمت را در ادامه ملاحظه خواهید کرد (در این کلاس از دو کلاس User و AppPath قسمت قبل نیز استفاده شده است):
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.CalculatedFields
{
    public class CalculatedFieldsPdfReport
    {
        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(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                            Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: 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.RainyDayTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 var listOfRows = new List<User>();
                 for (int i = 0; i < 220; i++)
                 {
                     listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                 }
                 dataSource.StronglyTypedList<User>(listOfRows);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "رکوردی یافت نشد.");
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
                 summary.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("ردیف", captionRotation: 90);
                 });

                 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(2);
                     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("CF1");
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var name = list.GetSafeStringValueOf<User>(x => x.Name);
                             var lastName = list.GetSafeStringValueOf<User>(x => x.LastName);
                             return name + " - " + lastName;
                         });
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });

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

                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF2");
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var balance = list.GetValueOf<User>(x => x.Balance);
                             return (long)balance * 3;
                         });
                     column.IsVisible(true);
                     column.Order(6);
                 });
             })
             .Export(export =>
             {
                 export.ToExcel(footer: "Footer text", header: "&24&U&\"Arial,Regular Bold\" New rpt.", pageLayoutView: true);
                 export.ToXml();
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptCalculatedFieldsSample.pdf"));
        }
    }
}

توضیحات:
- در این مثال از یک قلم سفارشی به نام Iranian Sans استفاده شده که در پوشه bin\fonts سورس به روز شده پروژه، قابل دریافت است.
- برخلاف قسمت قبل، از متد table.NumberOfDataRowsPerPage استفاده نشده است. به این ترتیب تعداد ردیف‌ها به صورت خودکار بر اساس اندازه صفحه محاسبه می‌شود.
- منبع داده تعریف شده توسط  dataSource.StronglyTypedList، بسیار مناسب است جهت کار با انواع و اقسام ORMها. تفاوتی نمی‌کند از چه ORM ایی استفاده می‌کنید؛ همینقدر که خروجی کار شما یک List باشد، در اینجا قابل استفاده خواهد بود. حتی نیازی هم به بانک اطلاعاتی نیست و در این مثال از یک منبع داده درون حافظه‌ای استفاده شده است.
- توضیحاتی در مورد ColumnsWidthsType :
برای تعیین عرض ستون‌ها، چهار حالت بر اساس مقادیر enum ایی به نام TableColumnWidthType میسر است:
الف) Relative : عرض نسبی. به این معنا که اگر سه ستون با عرض‌های 2, 1, 1 تعریف کنید، کل عرض صفحه به 4 قسمت تقسیم می‌شود. از این 4 قسمت، 2 قسمت به ستون اول و یک قسمت به ستون دوم و همچنین یک قسمت به ستون سوم اختصاص خواهد یافت.
ب) Absolute : در این حالت باید عرض ستون‌ها را دقیقا بر اساس user space units مشخص کنید.
ج) FitToContent : سعی خواهد کرد بر اساس طول محتوای یک سلول، عرض بهینه‌ای را محاسبه کند. در این حالت نیازی به قید column.Width نیست.
د) EquallySized : به صورت خودکار عرض تمام ستون‌ها را یکسان محاسبه می‌کند. در این حالت نیازی به قید column.Width نیست.
- اولین فیلد محاسباتی که در PdfReport به صورت توکار و خودکار در اختیار شما است، فیلد شماره ردیف می‌باشد که به صورت زیر مشخص می‌شود:
 column.IsRowNumber(true);
بنابراین نیازی نیست تا منبع داده شما شامل یک ستون اضافی به نام ردیف باشد. PdfReport این مورد را به صورت خودکار تولید خواهد کرد.
- سپس دو فیلد و ستون محاسباتی در گزارش فوق قابل مشاهده هستند:
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF1");
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var name = list.GetSafeStringValueOf<User>(x => x.Name);
                             var lastName = list.GetSafeStringValueOf<User>(x => x.LastName);
                             return name + " - " + lastName;
                         });
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });
برای اینکه یک فیلد پویا را به مجموعه فیلدهای مهیا شده توسط منبع داده اضافه کنیم، از متد CalculatedField با پارامتر اول مساوی true استفاده خواهیم کرد. سپس نیاز است نحوه محاسبه این فیلد را مشخص کنیم. امضای متد CalculatedField به صورت زیر است:
 public void CalculatedField(bool isCalculatedField, Func<IList<CellData>, object> calculatedFieldFormula)
به این معنا که توسط آرگومان دوم آن، لیست کلیه مقادیر ردیف جاری در اختیار شما قرار خواهد گرفت. در این بین فرصت خواهیم داشت بر این اساس، فیلد و مقدار جدیدی را تولید کرده و بازگشت دهیم؛ که نمونه‌ای از اینکار را در فیلد محاسبانی CF1 فوق مشاهده می‌کنید.
باید دقت داشت که نام خواص (column.PropertyName) باید منحصربفرد باشند و گرنه برنامه با یک استثناء متوقف خواهد شد. اگر ستون معرفی شده متناظر است با یک فیلد یا خاصیت منبع داده، باید PropertyName با رعایت کوچکی و بزرگی حروف، معادل فیلد متناظر باشد. اگر ستون تعریف شده یک فیلد محاسباتی است، تنها کافی است یک نام دلخواه غیرتکراری را ذکر کرد.
همچنین جهت سهولت کار، در فضای نام PdfRpt.Core.Helper، تعدادی متد برای کار با لیستی از CellDataها تدارک دیده شده‌اند؛ که نمونه‌ای از آن‌را در اینجا با استفاده از متدهای GetSafeStringValueOf ملاحظه می‌کنید.

- فیلد محاسباتی دیگری نیز در این گزارش قابل مشاهده است:
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("CF2");
                     column.HeaderCell("ف.م.");
                     column.Width(3);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CalculatedField(true,
                         list =>
                         {
                             if (list == null) return string.Empty;
                             var balance = list.GetValueOf<User>(x => x.Balance);
                             return (long)balance * 3;
                         });
                     column.IsVisible(true);
                     column.Order(6);
                 });
در اینجا در متد  column.CalculatedField، مقدار فیلد موجودی (Balance) ردیف جاری دریافت شده و سپس مقدار دلخواه جدیدی تولید و بازگشت داده شده است.
همچنین توسط متد column.AggregateFunction، مشخص کرده‌ایم که این ستون جدید نیاز به جمع پایین صفحه هم دارد. به علاوه توسط متد column.ColumnItemsTemplate، با مشخص سازی DisplayFormatFormula، پیش از نمایش اطلاعات فیلد جاری، مقدار آن‌را دریافت و فرمت کرده‌ایم؛ که در اینجا سه رقم جداکننده اعداد اضافه شده است.
ذکر متد template.TextBlock اختیاری است و حالت پیش فرض می‌باشد. قالب‌های دیگری نیز در اینجا تعریف شده‌اند که در مثال‌های قسمت‌های بعدی آن‌ها را بررسی خواهیم کرد (امکان نمایش تصویر، لینک، بارکد و غیره).

 
نظرات اشتراک‌ها
پیاده سازی generic repository یک ضد الگو است
خلاصه این مطلب خوب به این نحو است:
الف) اگر از یک Generic Repository داخل کدهای یک مثلا کنترلر مستقیما استفاده می‌کنید، فقط خودتان را گول زده‌اید!  این طراحی نشتی دارد و همچنین حد و مرز مشخصی را نمی‌توان برای آن قائل شد.
نشتی یا وابستگی که اینجا عنوان شده نمونه‌اش یک چنین متدی است:
IEnumerable<T> Find(Expression<Func<T, bool>> query);
در اینجا کار پیاده سازی query به یک لایه بالاتر (مانند یک کنترلر) محول شده که اشتباه است. حد و مرز لایه پیاده سازی کننده منطق تجاری شما باید مشخص باشد و نه باز. یک Expression فقط یک عبارت است که می‌توان با کمک آن انواع و اقسام کوئری‌های وابسته به فناوری دسترسی به داده‌های در حال استفاده را تهیه کرد. به عبارتی اگر تصور می‌کنید که در اینجا کپسوله سازی خوبی را انجام داده‌اید، اینطور نیست. چون دسترسی مستقیم به ORM خود را به لایه‌ای دیگر واگذار کرده‌اید و این دسترسی محصور نشده است.
نمونه خوبی را که مثال زده به صورت زیر است:
public IEnumerable<Customer> FindCustomerByName(string input)
    {
        return internalRepository.FindBy(c => c.Name == input);
    }
در اینجا دقیقا حد و مرز لایه تجاری و امکان دسترسی به آن مشخص است و دارای نشتی (باز بودن برای تغییرات بی حد و حصر در لایه‌های دیگر) نیست.
ب) الزامی به تعریف یک Generic Repository نیست اما اگر آن‌را تعریف کردید، باید این کلاس خاص داخل کدهایی که منطق تجاری برنامه را با حد و مرز مشخصی کپسوله می‌کنند استفاده کنید. نه اینکه آن‌را در خط مقدم کاری (مانند استفاده مستقیم در یک کنترلر) بکار بگیرید. به آن بیشتر به شکل یک سری متد کمکی نگاه کنید و بس. نامگذاری جالبی را هم در این حالت پیشنهاد داده: internalRepository 
مطالب
فراخوانی GraphQL API در یک کلاینت ASP.NET Core
در قسمت قبل، ایجاد کردن Mutation‌ها را در GraphQL تمام کردیم. در این قسمت تصمیم بر این است که از GraphQL API در یک برنامه ASP.NET Core استفاده کنیم. برای فراخوانی GraphQL API در یک برنامه ASP.NET Core، از یک کتابخانه ثالث که به ما در این فرآیند کمک می‌کند استفاده خواهیم کرد.

Preparing the ASP.NET Core Client Project 

کار را با ایجاد کردن یک پروژه ASP.NET Core Web API شروع می‌کنیم :
dotnet new api -n DotNetGraphQLClient
 
به محض اینکه پروژه را ایجاد کردیم فایل launchsettings.json را باز می‌کنیم و مقدار خصوصیت launchBrowser را به false  و خصوصیت applicationUrl  را به
https://localhost:5003;http://localhost:5004
تغییر می‌دهیم. در ادامه فایل appsettings.json را مطابق زیر ویرایش می‌کنیم (اضافه کردن کلید GraphQLURI ):
{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "GraphQLURI": "https://localhost:5001/graphql",
  "AllowedHosts": "*"
}

اکنون می‌توانیم کتابخانه مورد نیاز را نصب کنیم. در ترمینال مربوط به VS Code دستور زیر را وارد کنید:
dotnet add package GraphQL.Client
بعد از نصب، برای ثبت آن، کلاس Startup را مطابق زیر ویرایش کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(x => new GraphQL.Client.GraphQLClient(Configuration["GraphQLURI"]));
   ...
}

در سازنده کلاس GraphQLClient، آدرس endpoint را معرفی می‌کنیم (که در فایل appsettings.json می باشد) .
قدم بعد، ایجاد کردن یک کلاس به نام OwnerConsumer است که عملیات مربوط به فراخوانی API، در این کلاس می‌باشد. 
public class OwnerConsumer
{
    private readonly GraphQL.Client.GraphQLClient _client;
 
    public OwnerConsumer(GraphQL.Client.GraphQLClient client)
    {
        _client = client;
    }
}
سپس کلاس ایجاد شده را در متد ConfigureServices از کلاس Startup  ثبت می‌کنیم: 
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped(x => new GraphQL.Client.GraphQLClient(Configuration["GraphQLURI"]));
    services.AddScoped<OwnerConsumer>();
    ...
}


Creating Model Classes 
 
در قسمت اول تعدادی کلاس را در پوشه Models  ایجاد کردیم. همه آن کلاس‌ها را برای این client application نیاز داریم؛ پس آن‌ها را ایجاد می‌کنیم: 
public enum TypeOfAccount
{
    Cash,
    Savings,
    Expense,
    Income
}
public class Account
{
    public Guid Id { get; set; }
    public TypeOfAccount Type { get; set; }
    public string Description { get; set; }
}
public class Owner
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
 
    public ICollection<Account> Accounts { get; set; }
}

در قسمت سوم ( GraphQL Mutation ) یک کلاس Input type را برای اکشن‌های Mutation ایجاد کردیم. این کلاس هم مورد نیاز می‌باشد. پس آن را ایجاد می‌کنیم:
public class OwnerInput
{
    public string Name { get; set; }
    public string Address { get; set; }
}
اکنون همه چیز آماده است تا Query و Mutation ‌ها را ایجاد کنیم. 

Creating Queries and Mutations to Consume GraphQL API 

کلاس OwnerConsumer را باز می‌کنیم و متد GetAllOwners  را به آن اضافه می‌کنیم:
public async Task<List<Owner>> GetAllOwners()
{
    var query = new GraphQLRequest
    {
        Query = @"
                query ownersQuery{
                  owners {
                    id
                    name
                    address
                    accounts {
                      id
                      type
                      description
                    }
                  }
                }"
    };
 
    var response = await _client.PostAsync(query);
    return response.GetDataFieldAs<List<Owner>>("owners");
}

در ابتدا یک شیء جدید از نوع GraphQLRequest را ایجاد می‌کنیم که شامل خصوصیت Query که برای ارسال queryی که می‌خواهیم به GraphQL API ارسال کنیم، می‌باشد. این query مثل همانی که در ابزار UI.Playground اجرا کردیم، می‌باشد. جهت اجرای این query، متد PostAsync را فراخوانی می‌کنیم که یک پاسخ را برگشت می‌دهد. در نهایت می‌توان نتیجه را تبدیل به نوع مورد نیاز، با استفاده از متد GetDataFieldsAs کرد. 
توجه کنید که آرگومان متد GetDataFieldAs باید با نام query مطابقت داشته باشد .


Implementing a Controller 
یک کنترلر جدید را به نام OwnerController ایجاد می‌کنیم و آن را مطابق زیر ویرایش می‌کنیم: 
[Route("api/owners")]
public class OwnerController: Controller
{
    private readonly OwnerConsumer _consumer;
 
    public OwnerController(OwnerConsumer consumer)
    {
        _consumer = consumer;
    }
 
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        var owners = await _consumer.GetAllOwners();
        return Ok(owners);
    }
}

مثال ضمیمه شده در پایان قسمت قبل را اجرا کنید که بر روی پورت 5001 می‌باشد و سپس پروژه را اجرا کنید ( بر روی پورت 5003 است ).
اکنون می‌توانیم آن را در Postman تست کنیم.
جهت بازیابی تمامی owner ‌ها، در خواست زیر را در Postman صادر کنید:


جهت ایجاد یک owner جدید، کلاس OwnerConsumer را مطابق زیر ویرایش می‌کنیم ( اضافه کردن متد  CreateOwner ) :
 public async Task<Owner> CreateOwner(OwnerInput ownerToCreate)
 {
     var query = new GraphQLRequest
     {
         Query = @"
         mutation($owner: ownerInput!){
           createOwner(owner: $owner){
             id,
             name,
             address
           }
         }",
         Variables = new { owner = ownerToCreate }
     };

     var response = await _client.PostAsync(query);
     return response.GetDataFieldAs<Owner>("createOwner");
 }
و سپس ، کلاس OwnerController را باز می‌کنیم و متد PostItem را به آن اضافه می‌کنیم :
[HttpPost]
public async Task<IActionResult> PostItem([FromBody]OwnerInput model)
{
    var result = await _consumer.CreateOwner(model);
    return Json(result);
}

 اکنون، در خواست ایجاد یک owner جدید را در Postman به صورت زیر صادر می‌کنیم: 


همانطور که مشخص است، همه چیز به درستی کار می‌کند. شما می‌توانید مابقی Query و Mutation ‌ها را با استفاده از Postman تست کنید.

نتیجه گیری 
در این قسمت یادگرفتیم که چگونه :
  1. یک کلاینت ASP.NET Core را برای فراخوانی GraphQL API  آماده سازی کنیم. 
  2. چه کتابخانه‌هایی مورد نیاز است که می‌توانند به ما در انجام فرآیند فراخوانی GraphQL API کمک کنند.
  3. ایجاد کردن query‌ها و mutation‌ها در یک کلاینت ASP.NET Core. 

کد‌های کامل این قسمت را از اینجا دریافت کنید: DotNetGraphQLClient_4.zip 
کد‌های کامل قسمت قبل را می‌توانید از اینجا دریافت کنید:  ASPCoreGraphQL_3.rar

نظرات مطالب
مروری بر سازنده‌ها - سازنده‌های ایستا (static)
توضیح خوبی هم در این اینجا داده شده است.
یک سازنده استاتیک هنگام دسترسی به یکی از اعضای استاتیک آن کلاس (متد یا پراپرتی) یا قبل از اولین وهله سازی در طول چرخه حیات برنامه فقط یکبار اجرا خواهد شد.بنابراین کارهایی که قرار است برای کلاس فقط یکبار انجام شود در این قسمت قرار می‌دهیم.
مطالب
CoffeeScript #8

اصطلاحات عمومی CoffeeScript

Includes

برای چک کردن وجود یک مقدار در یک آرایه به طور معمول از indexOf استفاده می‌شود؛ در حالی که تمامی نسخه‌های IE به طور کامل از آن پشتیبانی نمی‌کنند.

var included = (array.indexOf("test") != -1)
CoffeeScript برای حل این مشکل، کلمه‌ی کلیدی in را ارائه کرده است:
included = "test" in array
متاسفانه برای چک کردن یک کلمه در یک متن می‌بایست از indexOf استفاده کرد و از کلمه‌ی کلیدی in نمی‌توان استفاده کرد. همچنین در صورتیکه بخواهید نبود چیزی را چک کنید نیز باید از indexOf استفاده کنید.
included = "a long test string".indexOf("test") isnt -1
و روش بهتر بجای مقایسه با مقدار 1-، استفاده از هک‌های اپراتور بیتی است:
string   = "a long test string"
included = !~ string.indexOf "test"

تکرار Propertyها

در صورتی که به خصوصیات یک شیء چندین بار نیاز داشته باشید، در جاوااسکریپت باید از کلمه‌ی کلیدی in استفاده کنید:

var object = {one: 1, two: 2}
for(var key in object) alert(key + " = " + object[key])
در حالیکه برای پیاده سازی توسط CoffeeScript باید از کلمه‌ی کلیدی of استفاده کرد:
object = {one: 1, two: 2}
alert("#{key} = #{value}") for key, value of object
نتیجه‌ی کامپایل آن می‌شود:
var key, object, value;

object = {
  one: 1,
  two: 2
};

for (key in object) {
  value = object[key];
  alert(key + " = " + value);
}
همانطور که در مثال بالا مشاهده می‌کنید، شما می‌توانید برای دسترسی به کلید و مقدار خصوصیات موجود در شیء، متغیری را برای هر کدام تعریف کنید که ما در اینجا از key  و value استفاده کرده‌ایم.

Min/Max

درست است که این تکنیک مخصوص CoffeeScript نیست، اما اشاره به آن می‌تواند مفید باشد. تابع Math.max و Max.min می‌توانند چندین آرگومان یا یک آرایه را به عنوان ورودی گرفته و بر روی آن محاسبات خود را انجام داده و خروجی را نشان دهند:

Math.max [14, 35, -7, 46, 98]... # 98
Math.min [14, 35, -7, 46, 98]... # -7
نتیجه‌ی آن پس از کامپایل می‌شود:
Math.max.apply(Math, [14, 35, -7, 46, 98]);

Math.min.apply(Math, [14, 35, -7, 46, 98]);
نکته: در صورتیکه آرگومان‌ها یا تعداد خانه‌های آرایه ارسالی زیاد باشند، چون مرورگرها محدودیتی را در تعداد پارامترهای ارسالی به یک تابع دارند، خروجی تولید نخواهد شد.

مطالب دوره‌ها
بررسی حالت‌های مختلف نصب RavenDB
چهار روش مختلف برای نصب، استفاده و توزیع RavenDB وجود دارند. ساده‌ترین روش آن‌را که اجرای فایل Raven.Server.exe است، تاکنون بررسی کردیم. این روش صرفا جهت دیباگ و کار برنامه نویسی مناسب است. در ادامه سه روش دیگر را بررسی خواهیم کرد.

الف) استفاده از RavenDB در حالت مدفون شده یا Embedded

حالت Embedded به این معنا است که RavenDB درون پروسه برنامه شما اجرا خواهد شد و نه به صورت پروسه‌ای مجزا. این حالت برای ارائه ساده برنامه‌های دسکتاپ بسیار مناسب است؛ یا حتی توزیع برنامه‌های سبک ASP.NET بدون نیاز به نصب بانک اطلاعاتی خاصی بر روی وب سرور.
برای کار با RavenDB در حالت Embedded ابتدا فایل‌های مورد نیاز آن‌را از طریق نیوگت دریافت کنید:
 PM> Install-Package RavenDB.Embedded -Pre
در این حالت فایل کلاینت مورد نیاز، اسمبلی Raven.Client.Embedded.dll خواهد بود. سپس در کدهای قبلی خود بجای استفاده از new DocumentStore، اینبار خواهیم داشت new EmbeddableDocumentStore.
 var documentStore = new EmbeddableDocumentStore { DataDirectory = @"~/app_data/ravendb" };
documentStore.Initialize();
سایر قسمت‌های برنامه نیازی به تغییر نخواهند داشت.
امکان تعریف DataDirectory در فایل کانفیگ برنامه نیز وجود دارد. فقط در این حالت باید دقت داشت که نام مسیر، با DataDir شروع می‌شود و نه DataDirectory :
<connectionStrings>
  <add name="Local" connectionString="DataDir = ~\Data"/>
سپس همانند قبل، مقدار خاصیت رشته اتصالی EmbeddableDocumentStore به نام مدخل فوق باید تنظیم گردد.


چند نکته جالب در مورد حالت Embedded
- امکان اجرای درون حافظه‌ای RavenDB نیز وجود دارد:
 var documentStore = new EmbeddableDocumentStore{RunInMemory = true}.Initialize()
در اینجا فقط کافی است خاصیت RunInMemory شیء EmbeddableDocumentStore به true تنظیم شود. این مورد بسیار مناسب است برای انجام آزمون‌های واحد بسیار سریع که پس از پایان کار برنامه، اثری از بانک اطلاعاتی آن باقی نخواهد ماند.
- اجرای حالت Embedded به صورت Embedded HTTP:
در حالت Embedded دیگر دسترسی به برنامه سیلورلایت Raven studio وجود ندارد. اگر علاقمند به کار با آن بودید، خاصیت UseEmbeddedHttpServer شیء EmbeddableDocumentStore را به true تنظیم کنید. سپس فایل Raven.Studio.xap را در ریشه وب سایت خود قرار دهید. اکنون مانند قبل آدرس localhost:8080/raven/studio.html برقرار خواهد بود.
همچنین سرور Http این بانک اطلاعاتی را نیز می‌توان دستی راه اندازی کرد. متد NonAdminHttp.EnsureCanListenToWhenInNonAdminContext بررسی می‌کند که آیا برنامه مجوز راه اندازی یک سرور را بر روی پورت مثلا 8080 دارد یا خیر.
 NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(8080);

// Start the HTTP server manually
var server = new RavenDbHttpServer(documentStore.Configuration, documentStore.DocumentDatabase);
server.Start();


ب) نصب RavenDB به صورت سرویس ویندوز NT
اگر مایل باشیم تا RavenDB را نیز مانند SQL Server به صورت یک سرویس ویندوز NT نصب کنیم تا همواره در پس زمینه سرور در حال اجرا باشد، کنسول پاورشل ویندوز را گشوده و سپس فرمان ذیل را صادر کنید:
 d:\ravendb\server> .\raven.server.exe /install
اکنون اگر به کنسول مدیریتی سرویس‌های ویندوز یا services.msc مراجعه کنید، ravendb را به صورت یک آیتم جدید در لیست سرویس‌های ویندوز خواهید یافت.
و اگر خواستید این سرویس را عزل کنید، دستور ذیل را در پاورشل ویندوز صادر کنید:
 d:\ravendb\server> .\raven.server.exe /uninstall


ج) نصب RavenDB به صورت یک پروسه IIS (یا اجرا شده توسط IIS)

فایل‌های مورد نیاز حالت اجرای RavenDB را به صورت یک پروسه مجزای IIS از نیوگت دریافت کنید:
 PM> Install-Package RavenDB.AspNetHost -Pre
در این حالت، پوشه bin، فایل xap و فایل کانفیگ برنامه وب مورد نیاز دریافت خواهند شد. پس از آن، تنها کافی است یک دایرکتوری مجازی را در IIS به این پوشه جدید اختصاص داده و همچنین بهتر است یک Application pool جدید را نیز برای آن تهیه کنید تا واقعا این برنامه در پروسه‌ی مجزای خاص خودش اجرا شود. حتی در این حالت می‌توان شماره پورت دیگری را به این برنامه اختصاص داد. به علاوه در این حالت تنظیمات Recycling مربوط به IIS را هم باید مدنظر داشت (در قسمت تنظیمات Application pool برنامه) و مثلا تنظیم کرد که برنامه پس از چه مدت فعال نبودن از حافظه خارج شود.


یک نکته
تمام بسته‌های مورد نیاز را یکجا از آدرس http://ravendb.net/download نیز می‌توان دریافت کرد. در نگارش‌های جدید، بسته نصاب نیز برای این بانک اطلاعاتی تهیه شده است که برای نمونه توزیع آن‌را جهت حالت نصب در IIS ساده‌تر می‌کند.
مطالب
Blazor 5x - قسمت 20 - کار با فرم‌ها - بخش 8 - استفاده از یک کامپوننت ثالث HTML Editor
در این قسمت می‌خواهیم بجای دریافت اطلاعات توضیحات یک اتاق، توسط یک text area متداول، برای مثال از Quill rich text editor استفاده کنیم. برای این منظور می‌توان از کامپوننت Blazor محصور کننده‌ی آن به نام Blazored TextEditor کمک گرفت.


نصب کامپوننت Blazored TextEditor

ابتدا نیاز است بسته‌ی نیوگت آن‌را با اجرای دستور زیر، به پروژه‌ی Blazor خود اضافه کرد:
dotnet add package Blazored.TextEditor
و همچنین کتابخانه‌ی اصلی quill را نیز در مسیر wwwroot/lib/quill نصب می‌کنیم:
libman install quill --provider unpkg --destination wwwroot/lib/quill
سپس به فایل Pages\_Host.cshtml مراجعه کرده و ابتدا مداخل تعریف فایل‌های CSS آن‌را اضافه می‌کنیم:
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServer.App</title>
    <base href="~/" />
    <link href="lib/quill/dist/quill.snow.css" rel="stylesheet" />
    <link href="lib/quill/dist/quill.bubble.css" rel="stylesheet" />
و در ادامه سه مدخل اسکریپتی زیر را نیز به قسمت پیش از بسته شدن تگ body، اضافه می‌کنیم:
 <script src="lib/quill/dist/quill.min.js"></script>
<script src="_content/Blazored.TextEditor/quill-blot-formatter.min.js"></script>
<script src="_content/Blazored.TextEditor/Blazored-BlazorQuill.js"></script>
<script src="_framework/blazor.server.js"></script>
</body>
اگر برنامه‌ی مورد نظر از نوع Blazor WASM است، این تنظیمات به فایل wwwroot\index.html منتقل می‌شوند.

و در آخر جهت سهولت کار با این کامپوننت می‌توان فضای نام آن‌را به فایل BlazorServer.App\_Imports.razor به صورت زیر اضافه کرد:
@using Blazored.TextEditor


استفاده از کامپوننت Blazored.TextEditor در کامپوننت HotelRoomUpsert.razor

می‌خواهیم در کامپوننت HotelRoomUpsert.razor مثال این سری، بجای کامپوننت InputTextArea مورد استفاده، از یک HTML Editor استفاده کنیم:
<div class="form-group">
    <label>Details</label>
    @*<InputTextArea @bind-Value="HotelRoomModel.Details" class="form-control"></InputTextArea>*@
    <BlazoredTextEditor @ref="@QuillHtml">
        <ToolbarContent>
            <select class="ql-header">
                <option selected=""></option>
                <option value="1"></option>
                <option value="2"></option>
                <option value="3"></option>
                <option value="4"></option>
                <option value="5"></option>
            </select>
            <span class="ql-formats">
                <button class="ql-bold"></button>
                <button class="ql-italic"></button>
                <button class="ql-underline"></button>
                <button class="ql-strike"></button>
            </span>
            <span class="ql-formats">
                <select class="ql-color"></select>
                <select class="ql-background"></select>
            </span>
            <span class="ql-formats">
                <button class="ql-list" value="ordered"></button>
                <button class="ql-list" value="bullet"></button>
            </span>
            <span class="ql-formats">
                <button class="ql-link"></button>
            </span>
        </ToolbarContent>
        <EditorContent>
        </EditorContent>
    </BlazoredTextEditor>
</div>
- در اینجا قسمت محتوای EditorContent مثال آن‌را خالی کرده‌ایم.
- همانطور که ملاحظه می‌کنید، این تعریف به همراه یک ارجاع به وهله‌ای از آن نیز هست:
<BlazoredTextEditor @ref="@QuillHtml">
به همین جهت نیاز است فیلد متناظر با آن‌را در قسمت کدهای کامپوننت، به صورت زیر تعریف کرد:
@code
{
   private BlazoredTextEditor QuillHtml;
تا اینجا اگر برنامه را اجرا کنیم، به خروجی زیر می‌رسیم:


برای تغییر اندازه و مقدار placeholder پیش‌فرض آن، می‌توان به صورت زیر عمل کرد:
<div class="form-group pb-4" style="height:250px;">
    <label>Details</label>
    <BlazoredTextEditor @ref="@QuillHtml" Placeholder="Please enter the room's detail">


تنظیم و دریافت متن نمایشی HTML Editor

مطابق مستندات این کامپوننت، روش تنظیم متن نمایشی آن، به کمک متد LoadHTMLContent است. به همین جهت متد زیر را به کدهای کامپوننت جاری اضافه می‌کنیم:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
        }
    }
بنابراین روش متداول two-way binding در اینجا کار نمی‌کند و باید متن این ادیتور را به نحو فوق تنظیم کرد و برای مثال در زمان بارگذاری اولیه‌ی این کامپوننت و در حالت ویرایش، متن دریافتی از بانک اطلاعاتی را به ادیتور فوق ارسال نمود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
            await SetHTMLAsync();
        }

        // ... 
    }
و یا در زمان ثبت اولیه و یا حتی در حالت ویرایش اطلاعات در متد HandleHotelRoomUpsert، با استفاده از متد GetHTML آن، خاصیت HotelRoomModel.Details را مقدار دهی اولیه کرد:
    private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       HotelRoomModel.Details = await QuillHtml.GetHTML();

       // ...
    }

مشکل! ادیتور در زمان ویرایش یک رکورد، اطلاعات پیشین را نمایش نمی‌دهد!

پس از اعمال تغییرات فوق، برنامه را اجرا می‌کنیم. سپس یک اتاق جدید را اضافه کرده و در لیست نمایش اتاق‌ها، گزینه‌ی ویرایش آن‌را انتخاب می‌کنیم. در این حالت هرچند کار مقدار دهی HotelRoomModel.Details در زمان ثبت اطلاعات انجام شده، اما ... در زمان ویرایش چیزی نمایش داده نمی‌شود و تغییراتی را که به متد رویدادگردان OnInitializedAsync اضافه کرده‌ایم، عمل نمی‌کنند.
در این مورد در قسمت بررسی چرخه‌ی حیات کامپوننت‌ها توضیحاتی ابتدایی ارائه شد:
«رویدادهای OnAfterRender و OnAfterRenderAsync

پس از هر بار رندر کامپوننت، این متدها فراخوانی می‌شوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آن‌ها به پایان رسیده‌است. یکی از کاربردهای آن، آغاز کامپوننت‌های جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.»

بنابراین در این حالت خاص که ادیتور جاوا اسکریپتی مورد استفاده، پس از رندر کامل UI نمایش داده می‌شود، قرار دادن متد SetHTML در روال رویدادگردان OnInitializedAsync کار نخواهد کرد و باید آن‌را به روال رویدادگردان OnAfterRender انتقال دهیم:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
   await SetHTMLAsync();
}
پس از این تغییرات هم باز متن وارد شده‌ی در قسمت توضیحات، در حالت ویرایش نمایش داده نمی‌شود! علت آن‌را نیز در مطلب بررسی چرخه‌ی حیات کامپوننت‌ها بررسی کردیم: «یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.» به همین جهت نیاز به فراخوانی دستی StateHasChanged وجود دارد:
    private async Task SetHTMLAsync()
    {
        if(!string.IsNullOrEmpty(HotelRoomModel.Details))
        {
            await QuillHtml.LoadHTMLContent(HotelRoomModel.Details);
            StateHasChanged();
        }
    }


مشکل! اگر در این حالت سعی کنیم متنی را در ادیتور وارد کنیم، میسر نیست و همچنین CPU Usage سیستم به 100 درصد رسیده‌است!

علت اینجا است که فراخوانی StateHasChanged، هر چند سبب رندر مجدد UI می‌شود، اما چون در پایان کار رندر قرار داریم، یک حلقه‌ی بی‌نهایت را سبب خواهد شد. به همین جهت باید در متد OnAfterRenderAsync، بر اساس پارامتر firstRender، از رندرهای بعدی جلوگیری کرد:
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
        {
            return;
        }

        while (true)
        {
            try
            {
                await SetHTMLAsync();
                break;
            }
            catch
            {
                await Task.Delay(100); // Quill needs some time to load
            }
        }
    }
در اینجا هم مدیریت firstRender را مشاهده می‌کنید، تا دیگر یک حلقه‌ی بی‌نهایت رخ ندهد و هم حلقه‌ای را جهت منتظر ماندن تا بارگذاری کامل Quill در این مثال. این افزونه‌ی جاوا اسکریپتی، حتی پس از پایان رندر کامپوننت هم نیاز به مدت زمانی دارد تا بتواند کامل بارگذاری شده و قابل استفاده شود.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-20.zip