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

صورت مساله

می‌خواهیم اطلاعات نمایش داده شده در گرید را به نحوی فرمت کنیم که
الف) اگر id ردیف مساوی 5 بود، رنگ و پس زمینه‌ی آن تغییر کند.
ب) نام محصول، به جزئیات آن لینک شود و این اطلاعات توسط یک jQuery UI Dialog نمایش داده شود.
ج) عدد قیمت با سه رقم جدا کننده همراه باشد.




تکمیل مدل برنامه

مدل قسمت اول صرفا یک محصول بود. مدل قسمت جاری، اطلاعات تولید/تامین کننده آن‌را توسط کلاس Supplier نیز به همراه دارد:
namespace jqGrid02.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public Supplier Supplier { set; get; }
    }

    public class Supplier
    {
public int Id { set; get; }
        public string CompanyName { set; get; }
        public string Address { set; get; }
        public string PostalCode { set; get; }
        public string City { set; get; }
        public string Country { set; get; }
        public string Phone { set; get; }
        public string HomePage { set; get; }
    }
}

کدهای سمت سرور

کدهای سمت سرور مانند متد GetProducts به همراه صفحه بندی و مرتب سازی پویای آن دقیقا مانند قسمت قبل است.
در اینجا فقط یک اکشن متد جدید جهت بازگشت اطلاعات تولید کننده‌ای مشخص با فرمت JSON، اضافه شده‌است:
        public ActionResult GetGetSupplierData(int id)
        {
            var list = ProductDataSource.LatestProducts;
            var product = list.FirstOrDefault(x => x.Id == id);
            if (product == null)
                return Json(null, JsonRequestBehavior.AllowGet);

            return Json(new
                          {
                              product.Supplier.CompanyName,
                              product.Supplier.Address,
                              product.Supplier.PostalCode,
                              product.Supplier.City,
                              product.Supplier.Country,
                              product.Supplier.Phone,
                              product.Supplier.HomePage
                          }, JsonRequestBehavior.AllowGet);
        }

کدهای سمت کلاینت

صفحه دیالوگی که قرار است اطلاعات تولید کننده را نمایش دهد، یک چنین ساختاری دارد:
<div dir="rtl" id="supplierDialog">
    <span id="CompanyName"></span><br /><br />
    <span id="Address"></span><br />
    <span id="PostalCode"></span>, <span id="City"></span><br />
    <span id="Country"></span><br /><br />
    <span id="Phone"></span><br />
    <span id="HomePage"></span>
</div>
و تغییرات گرید برنامه به شرح زیر است:
    <script type="text/javascript">
        function showSupplierDialog(linkElement, supplierId) {
            //request json data
            $.getJSON('@Url.Action("GetGetSupplierData","Home")', { id: supplierId }, function (data) {
                //set values in dialog
                for (var property in data) {
                    if (data.hasOwnProperty(property)) {
                        $('#' + property).text(data[property]);
                    }
                }
                
                //get link position
                var linkPosition = $(linkElement).offset();
                $('#supplierDialog').dialog('option', 'position', [linkPosition.left, linkPosition.top]);
                //open dialog
                $('#supplierDialog').dialog('open');
            });
        }

        $(document).ready(function () {

            $('#supplierDialog').dialog({
                 autoOpen: false, bgiframe: true, resizable: false, title: 'تولید کننده'
            });

            $('#list').jqGrid({
                // .... مانند قبل
                colNames: ['شماره', 'نام محصول', 'قیمت'],
                //columns model
                colModel: [
                    {
                        name: 'Id', index: 'Id', align: 'right', width: 20,
                        formatter: function (cellvalue, options, rowObject) {
                            var cellValueInt = parseInt(cellvalue);
                            if (cellValueInt == 5) {
                                return "<span style='background: brown; color: yellow'>" + cellvalue + "</span>";
                            }
                            return cellvalue;
                        }
                    },
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 300,
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='#' onclick='showSupplierDialog(this, " + rowObject[0] + ");'>" + cellvalue + "</a>";
                        }
                    },
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 50,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.', thousandsSeparator: ',', decimalPlaces: 2, prefix: '$'
                        }
                    }
                ],
                // .... مانند قبل
            });
        });
    </script>
- همانطور که ملاحظه می‌کنید، توسط خاصیت formatter می‌توان عناصر در حال نمایش را فرمت کرد و بر روی نحوه‌ی نمایش نهایی آن‌ها تاثیرگذار بود.
در حالت ستون Id، از یک formatter سفارشی استفاده شده‌است. در اینجا این فرمت کننده به صورت یک callback عمل کرده و پیش از رندر نهایی اطلاعات، مقدار سلول جاری را توسط cellvalue در اختیار ما قرار می‌دهد. در این بین هر نوع فرمتی را که نیاز است می‌توان اعمال کرد و سپس یک رشته را بازگشت می‌دهیم. این رشته در سلول جاری درج خواهد شد.
- اگر مانند ستون Name، نیاز به مقادیر سایر سلول‌ها نیز وجود داشت، می‌توان از آرایه‌ی rowObject استفاده کرد. برای مثال در این حالت، یک لینک که کلیک بر روی آن سبب فراخوانی تابع showSupplierDialog می‌شود، در سلول‌های ستون Name درج خواهند شد. اولین rowObject که در اینجا مورد استفاده است، به ستون اول یا همان Id محصول اشاره می‌کند.
- در ستون Price از یک سری formatter از پیش تعریف شده استفاده شده‌است. نمونه‌ای از آن را در قسمت اول در ستون نمایش وضعیت موجود بودن محصول با تنظیم formatter: checkbox مشاهده کرده‌اید. در اینجا از یک formatter توکار دیگر به نام currency برای کار با مقادیر پولی استفاده شده‌است به همراه تنظیمات خاص آن.
- متد showSupplierDialog طوری تنظیم شده‌است که پس از دریافت Id یک محصول، آن‌را به سرور ارسال کرده و مشخصات تولید کننده‌ی آن‌را با فرمت JSON دریافت می‌کند. سپس در حلقه‌ای که مشاهده می‌کنید، خواص شیء جاوا اسکریپتی دریافتی استخراج و به spanهای supplierDialog انتساب داده می‌شوند. جهت سهولت کار، Id این spanها دقیقا مساوی Id خواص شیء دریافتی از سرور، درنظر گرفته شده‌اند.
- در مورد راست به چپ نمایش داده شدن عنوان دیالوگ، تغییرات CSS ایی لازم است که در قسمت اول بیان شدند.


برای مطالعه بیشتر
لیست کامل فرمت کننده‌های توکار
فرمت کننده‌های سفارشی


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid02.zip
 
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
همانطور که در قسمت قبل، با معرفی مقدماتی Middlewareها عنوان شد، تمام قابلیت‌های یک برنامه‌ی ASP.NET Core، به صورت پیش فرض غیرفعال هستند؛ مگر آنکه Middlewareهای مخصوص آن‌ها را به صورت دستی و با آگاهی کامل، به کلاس آغازین برنامه اضافه کنید. در این قسمت قصد داریم تعداد دیگری از این Middlewareهای توکار را مورد بررسی قرار دهیم.


فعال سازی پردازش فایل‌های استاتیک در برنامه‌های ASP.NET Core 1.0

در مورد پوشه‌ی جدید wwwroot در «قسمت 2 - بررسی ساختار جدید Solution» مطالبی عنوان شدند. جهت یادآوری:
اگر فایل Program.cs را بررسی کنید، یک چنین تعاریفی را مشاهده خواهید کرد:
public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();
 
        host.Run();
    }
}
در کدهای فوق، سطر UseContentRoot، پوشه‌ی خاصی را به نام content root معرفی می‌کند که در اینجا به همان پوشه‌ی اصلی برنامه اشاره می‌کند و پوشه‌ی wwwroot از مسیر content root/wwwroot خوانده می‌شود که جهت ارائه‌ی تمام فایل‌های عمومی برنامه مورد استفاده قرار می‌گیرد (مانند تصاویر، فایل‌های JS ،CSS و امثال آن). هدف این است که کدهای سمت سرور برنامه (قرار گرفته در content root) از کدهای عمومی آن (قرار گرفته در پوشه‌ی ویژه‌ی content root/wwwroot) جدا شده و به این ترتیب احتمال نشتی اطلاعات سمت سرور به حداقل برسد.

یک مثال: زمانیکه فایل استاتیک images/banner3.svg در پوشه‌ی wwwroot قرار می‌گیرد، با آدرس http://localhost:9189/images/banner3.svg توسط عموم قابل دسترسی خواهد بود.

یک نکته‌ی امنیتی مهم
در برنامه‌های ASP.NET Core، هنوز فایل web.config را نیز مشاهده می‌کنید. این فایل تنها کاربردی که در اینجا دارد، تنظیم ماژول AspNetCoreModule برای IIS است تا IIS static file handler آن، راسا اقدام به توزیع فایل‌های یک برنامه‌ی ASP.NET Core نکند. بنابراین توزیع این فایل را بر روی سرورهای IIS فراموش نکنید. همچنین بهتر است در ویندوزهای سرور، به قسمت Modules feature مراجعه کرده و StaticFileModule را از لیست ویژگی‌های موجود حذف کرد.


نصب Middleware مخصوص پردازش فایل‌های استاتیک

در قسمت قبل با نحوه‌ی نصب و فعال سازی middleware مخصوص WelcomePage آشنا شدیم. روال کار در اینجا نیز دقیقا به همان صورت است:
الف) نصب بسته‌ی نیوگت Microsoft.AspNetCore.StaticFiles
برای اینکار می‌توان بر روی گره‌ی references کلیک راست کرده و سپس از منوی ظاهر شده،‌گزینه‌ی manage nuget packages را انتخاب کرد. سپس ابتدا برگه‌ی browse را انتخاب کنید و در اینجا نام Microsoft.AspNetCore.StaticFiles را جستجو کرده و سپس نصب کنید.


انجام این کارها معادل افزودن یک سطر ذیل به فایل project.json است و سپس ذخیره‌ی آن که کار بازیابی بسته‌ها را به صورت خودکار آغاز می‌کند:
 "dependencies": {
   // same as before
    "Microsoft.AspNetCore.StaticFiles": "1.0.0"
},
ب) معرفی Middleware پردازش فایل‌های استاتیک
برای اینکار به فایل Startup.cs مراجعه کرده و سطر UseStaticFiles را به متد Configure اضافه کنید (به UseWelcomePage هم دیگر نیازی نداریم):
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }
 
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();
        //app.UseWelcomePage();
 
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello DNT!");
        });
    }
}

یک مثال: بر روی پوشه‌ی wwwroot کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید. سپس یک HTML page جدید را به نام index.html به این پوشه اضافه کنید.
با این محتوا:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Hello World</title>
</head>
<body>
    Hello World!
</body>
</html>
در این حالت برنامه را اجرا کنید. خروجی ذیل را مشاهده خواهید کرد:


که این خروجی دقیقا خروجی app.Run برنامه است و نه محتوای فایل index.html ایی که اضافه کردیم.
در ادامه اگر مسیر کامل این فایل را (http://localhost:7742/index.html) درخواست دهیم، آنگاه می‌توان خروجی این فایل استاتیک را مشاهده کرد:


این رفتار اندکی متفاوت است نسبت به نگارش‌های قبلی ASP.NET که فایل index.html را به عنوان فایل پیش فرض، درنظر می‌گرفت و محتوای آن‌را نمایش می‌داد. منظور از فایل پیش فرض، فایلی است که با درخواست ریشه‌ی یک مسیر، به کاربر ارائه داده می‌شود و index.html یکی از آن‌ها است.
برای رفع این مشکل، نیاز است Middleware مخصوص آن‌را به نام Default Files نیز به برنامه معرفی کرد:
public void Configure(IApplicationBuilder app)
{
   app.UseDefaultFiles();
   app.UseStaticFiles();
در این حالت است که با درخواست ریشه‌ی سایت، فایل پیش فرض آن نمایش داده خواهد شد:


فعال سازی Default Files، سبب جستجوی یکی از 4 فایل ذیل به صورت پیش فرض می‌شود (اگر تنها ریشه‌ی پوشه‌ای درخواست شود):
default.htm
default.html
index.htm
index.html

اگر خواستید فایل سفارشی خاص دیگری را معرفی کنید، نیاز است پارامتر DefaultFilesOptions آن‌را مقدار دهی نمائید:
 // Serve my app-specific default file, if present.
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);


ترتیب معرفی Middlewares مهم است

در قسمت قبل، در حین معرفی تفاوت‌های Middlewareها با HTTP Modules، عنوان شد که اینبار برنامه نویس می‌تواند بر روی ترتیب اجرای Middlewareها کنترل کاملی داشته باشد و این ترتیب معادل است با ترتیب معرفی آن‌ها در متد Configure، به نحوی که مشاهده می‌کنید. برای آزمایش این مطلب، متد معرفی middleware فایل‌های پیش فرض را پس از متد معرفی فایل‌های استاتیک قرار دهید:
public void Configure(IApplicationBuilder app)
{
  app.UseStaticFiles();
  app.UseDefaultFiles();
در این حالت اگر برنامه را اجرا کنید، به این خروجی خواهید رسید:


بله. اینبار تعریف فایل‌های پیش فرض، هیچ تاثیری نداشته و درخواست ریشه‌ی سایت، بدون ذکر صریح نام فایلی، مجددا به app.Run ختم شده‌است.


توزیع فایل‌های استاتیک خارج از wwwroot

همانطور که در ابتدای بحث عنوان شد، با فعال سازی UseStaticFiles به صورت پیش فرض مسیر content root/wwwroot در معرض دید دنیای خارج قرار می‌گیرد و توسط وب سرور قابل توزیع خواهد شد:
○ wwwroot
   § css
   § images
   § ...
○ MyStaticFiles
   § test.png
اما اگر قصد داشته باشیم تا تصویر test.png موجود در پوشه‌ی MyStaticFiles خارج از wwwroot را نیز عمومی کنیم چه باید کرد؟
برای این منظور می‌توان از پارامتر StaticFileOptions متد UseStaticFiles به نحو ذیل جهت معرفی پوشه‌ی MyStaticFiles استفاده کرد:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
    RequestPath = new PathString("/StaticFiles")
});
در این حالت، مسیر دسترسی عمومی به این فایل، به صورت  http://<app>/StaticFiles/test.png خواهد بود (بر مبنای RequestPath تنظیم شده).


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


فرض کنید پوشه‌ی تصاویر را به پوشه‌ی عمومی wwwroot اضافه کرده‌ایم. برای فعال سازی مرور محتوای این پوشه می‌توان از Middleware دیگری به نام DirectoryBrowser استفاده کرد:
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});
بعد از انجام اینکار به خطای ذیل خواهید رسید:
 Unable to resolve service for type 'System.Text.Encodings.Web.HtmlEncoder' while attempting to activate 'Microsoft.AspNetCore.StaticFiles.DirectoryBrowserMiddleware'.
برای رفع آن، سرویس آن نیز باید به متد ConfigureServices اضافه شود:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDirectoryBrowser();
}
در این حالت پس از اجرای برنامه، اگر مسیر http://localhost:7742/myimages را درخواست دهید (MyImages از RequestPath تنظیم شده، گرفته می‌شود)، به تصویر ذیل خواهید رسید:


مشکل! در این حالت که DirectoryBrowser را فعال کرده‌ایم، اگر بر روی لینک فایل تصویر نمایش داده شده کلیک کنیم، باز پیام Hello DNT یا اجرای app.Run را شاهد خواهیم بود.
به این دلیل که UseStaticFiles پیش فرض، مسیر درخواستی MyImages را که بر روی file system وجود ندارد، نمی‌شناسد. برای رفع این مشکل تنها کافی است مسیریابی این Request Path خاص را نیز فعال کنیم:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});


بررسی خلاصه‌ی تنظیماتی که به فایل آغازین برنامه اضافه شدند

تا اینجا اگر توضیحات را قدم به قدم دنبال و اجرا کرده باشید، یک چنین تنظیماتی را خواهید داشت:
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
 
namespace Core1RtmEmptyTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDirectoryBrowser();
        }
 
        public void Configure(IApplicationBuilder app)
        {
            app.UseDefaultFiles();
 
            app.UseStaticFiles(); // For the wwwroot folder
 
            // For the files outside of the wwwroot
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
                RequestPath = new PathString("/StaticFiles")
            });
 
            // For DirectoryBrowser
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            app.UseDirectoryBrowser(new DirectoryBrowserOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            //app.UseWelcomePage();
 
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello DNT!");
            });
        }
    }
}
services.AddDirectoryBrowser برای فعال سازی مرور پوشه‌ها اضافه شده‌است.
UseDefaultFiles کار فعال سازی شناسایی فایل‌های پیش فرضی مانند index.html را در صورت ذکر نام ریشه‌ی یک پوشه، انجام می‌دهد.
اولین UseStaticFiles تعریف شده، تمام مسیرهای فیزیکی ذیل wwwroot را عمومی می‌کند.
دومین UseStaticFiles تعریف شده، پوشه‌ی MyStaticFiles واقع در خارج از wwwroot را عمومی می‌کند.
سومین UseStaticFiles تعریف شده، پوشه‌ی فیزیکی wwwroot\images را به مسیر درخواست‌های MyImages نگاشت می‌کند (http://localhost:7742/myimages) تا توسط DirectoryBrowser تعریف شده، قابل استفاده شود.
در آخر هم DirectoryBrowser تعریف شده‌است.


یک نکته‌ی امنیتی مهم
یک چنین قابلیتی (مرور فایل‌های درون یک پوشه) به صورت پیش فرض بر روی تمام IIS‌ها به دلایل امنیتی غیرفعال است. به همین جهت بهتر است Middleware فوق را هیچگاه استفاده نکنید و به این قسمت صرفا از دیدگاه اطلاعات عمومی نگاه کنید.


ساده سازی تعاریف توزیع فایل‌های استاتیک

Middleware دیگری به نام FileServer کار تعریف توزیع فایل‌های استاتیک را ساده می‌کند. اگر آن‌را تعریف کنید:
 app.UseFileServer();
اینکار به معنای تعریف یکباره‌ی UseStaticFiles و UseDefaultFiles، با ترتیب صحیح آن‌ها است.
اگر خواستید DirectoryBrowsing آن‌را نیز فعال کنید، پارامتر ورودی آن‌را به true مقدار دهی کنید (که به صورت پیش فرض غیرفعال است):
 app.UseFileServer(enableDirectoryBrowsing: true);
همچنین در اینجا می‌توانید مسیر پوشه‌ی MyStaticFiles خارج از wwwroot را نیز با مقدار دهی پارامتر FileServerOptions آن، مشخص کنید:
app.UseFileServer(new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
        RequestPath = new PathString("/StaticFiles"),
        EnableDirectoryBrowsing = false
    });


توزیع فایل‌های ناشناخته

اگر به سورس ASP.NET Core 1.0 دقت کنید، کلاسی را به نام FileExtensionContentTypeProvider خواهید یافت. این‌ها پسوندها و mime typeهای متناظری هستند که توسط ASP.NET Core شناخته شده و توزیع می‌شوند. برای مثال اگر فایلی را به نام test.xyz به پوشه‌ی wwwroot اضافه کنید، درخواست آن توسط کاربر، به Hello DNT ختم می‌شود؛ چون در این کلاس پایه، پسوند xyz تعریف نشده‌است.
برای رفع این مشکل و تکمیل این لیست می‌توان به نحو ذیل عمل کرد:
 // Set up custom content types -associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".xyz"] = "text/html";
 
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
}) ; // For the wwwroot folder
در اینجا ابتدا همان کلاس پایه FileExtensionContentTypeProvider را نمونه سازی می‌کنیم و سپس به دیکشنری آن، پسوند و mime type ویژه‌ی خود را اضافه می‌کنیم. سپس این provider را می‌توان به خاصیت ContentTypeProvider پارامتر StaticFileOptions آن نسبت داد. اکنون این فایل با پسوند xyz، قابل شناسایی می‌شود:


و یا اگر خواستید کمی تمیزتر کار کنید، بهتر است از کلاس پایه FileExtensionContentTypeProvider ارث بری کرده و سپس در سازنده‌ی این کلاس، خاصیت Mappings را ویرایش نمود:
public class XyzContentTypeProvider : FileExtensionContentTypeProvider
{
    public XyzContentTypeProvider()
    {
        this.Mappings.Add(".xyz", "text/html");
    }
}
و برای استفاده‌ی از آن خواهیم داشت:
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = new XyzContentTypeProvider()
}) ; // For the wwwroot folder

روش دیگر مدیریت این مساله، تنظیم مقدار خاصیت ServeUnknownFileTypes به true است:
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "image/png"
});
در اینجا هر پسوند شناخته نشده‌ای با mime type تصویر png، توزیع خواهد شد. البته از لحاظ امنیتی توصیه شده‌است که چنین کاری را انجام ندهید و از این تنظیم عمومی نیز صرفنظر کنید.
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت هفتم - بررسی مفاهیم Mobx
MobX از 4 مفهوم اصلی تشکیل می‌شود:

- Observable state: در MobX نیز همانند Redux، کل شیء state به صورت یک شیء جاوا اسکریپتی ارائه می‌شود؛ با این تفاوت که در اینجا این شیء، یک Observable است که نمونه‌ای از مفهوم آن‌را در مثال قسمت قبل بررسی کردیم.
- Actions: متدهایی هستند که state را تغییر می‌دهند.
- Computed properties: در مورد مفهوم خواص محاسباتی در قسمت قبل بحث کردیم. این خواص، مقدار خود را بر اساس تغییرات سایر خواص Observable، به روز می‌کنند.
- Reactions: سبب بروز اثرات جانبی (side effects) می‌شوند؛ مانند تعامل با دنیای خارج. نمونه‌ای از آن، متد autorun است که تغییرات Observableها را ردیابی می‌کند.

برای مثال خاصیت محاسباتی fullName، تغییرات سایر خواص Observable را احساس کرده و مقدار خودش را به روز می‌کند. سپس یک Reaction به آن، می‌تواند به روز رسانی DOM، جهت نمایش این تغییرات باشد و یا نمونه‌ی دیگری که می‌تواند بسیاری از این مفاهیم را نمایش دهد، کلاس زیر است:
import { action, observable, computed } from 'mobx';

class PizzaCalculator {
  @observable numberOfPeople = 0;
  @observable slicesPerPerson = 2;
  @observable slicesPerPie = 8;

  @computed get slicesNeeded() {
    console.log('Getting slices needed');
    return this.numberOfPeople * this.slicesPerPerson;
   }

  @computed get piesNeeded() {
    console.log('Getting pies needed');
    return Math.ceil(this.slicesNeeded / this.slicesPerPie);
   }

   @action addGuest() {
     this.numberOfPeople!++;
   }
}
- دراینجا با استفاده از decorator syntax کتابخانه‌ی mobx، خواص و متدهای این کلاس معمولی ES6 را مزین کرده‌ایم.
- برای مثال زمانیکه تعریف observable numberOfPeople@ را داریم، به این معنا است که می‌خواهیم تغییرات تعداد افراد را تحت نظر قرار دهیم و اگر تغییری در مقدار آن صورت گرفت، آنگاه مقدار خواص محاسباتی که با computed@ مزین شده‌اند، به صورت خودکار به روز رسانی شوند.
- action@ به این معنا است که متدی در اینجا، سبب بروز تغییری در state کلاس جاری می‌شود. MobX به همراه یک strict mode است که اگر فعال باشد، ذکر تزئین کننده‌ی action@ بر روی یک چنین متدهایی ضروری است، در غیراینصورت، الزامی به درج آن نیست.

در این قطعه کد تعدای console.log را هم ملاحظه می‌کنید. علت آن نمایش مفهوم کش کردن اطلاعات در MobX است. فرض کنید برای بار اول، مقدار یکی از خواصی را که به صورت observable تعریف شده‌اند، تغییر می‌دهیم. در این حالت تمام خواص محاسباتی وابسته‌ی به آن‌ها، به صورت خودکار مجددا محاسبه شده و console.log‌ها را نیز مشاهده خواهیم کرد. اگر برای بار دوم همین فراخوانی صورت گیرد و تغییری در مقادیر خواص observable صورت نگیرد، MobX از نگارش کش شده‌ی این خواص محاسباتی استفاده می‌کند و بی‌جهت سبب رندر مجدد UI نخواهد شد که در نهایت کارآیی بالایی را سبب خواهد شد. برای پیاده سازی یک چنین قابلیتی با Redux باید از مفهومی مانند React.memo و Memoization و کتابخانه‌های کمکی مانند Reselect استفاده کرد؛ اما در اینجا به صورت توکار و خودکار اعمال می‌شود.


ساختارهای داده‌ای که توسط MobX پشتیبانی می‌شوند

MobX از اکثر ساختارهای داده‌ای متداول در جاوا اسکریپت پشتیبانی می‌کند؛ برای مثال:
- اشیاء مانند ({})observable
- آرایه‌ها مانند ([])observable
- Maps مانند observable(new Map())

چند نکته:
- همانطور که در قسمت قبل نیز ذکر شد، decorators در اصل یکسری تابع هستند و برای مثال می‌توان observable را به صورت observable@ و یا به صورت یک تابع معمولی مورد استفاده قرار داد.
- اگر شیء‌ای را به صورت ({})observable معرفی کنیم، با افزودن خواصی به آن پس از این فراخوانی، این خواص دیگر مورد ردیابی قرار نخواهند گرفت. علت آن‌را هم در شبه‌کد زیر می‌توان مشاهده کرد:
const extendObservable = (target, source) => {
  source.keys().forEach(key => {
    const wrappedInObservable = observable(source[key]);
    Object.defineProperty(target, key, {
      set: value.set.
      get: value.get
    });
  });
};
کاری که متد observable انجام می‌دهد، شمارش کلیدهای (خواص) شیء ارسالی به آن است و سپس محصور کردن آن‌ها درون یک شیء observable و در آخر بازگشت آن.
برای رفع این مشکل می‌توان از Map استفاده کرد. یعنی در اینجا اگر قرار است تعداد خواص اشیاء را به صورت پویا تغییر دهید، آن‌ها را به صورت Map تعریف کنید؛ چون متد set آن توسط observableها ردیابی می‌شود.


استفاده از MobX با React توسط کتابخانه‌ی mobx-react

تا اینجا MobX را به صورت متکی به خود مورد بررسی قرار دادیم. اکنون قصد داریم آن‌را به یک برنامه‌ی React متصل کنیم. برای اینکار کتابخانه‌های زیادی وجود دارند که در این قسمت کلیات روش کار با کتابخانه‌ی mobx-react را در بین آن‌ها بررسی می‌کنیم.

نصب کتابخانه‌ی mobx-react

ابتدا نیاز است تا این کتابخانه را نصب کنیم:
 > npm install --save mobx mobx-react

تحت نظر قرار دادن کامپوننت‌ها

در ادامه پس از نصب کتابخانه‌ی mobx-react، نیاز است کامپوننت‌ها را تحت نظر MobX قرار دهیم که اینکار را می‌توان توسط تزئین کننده‌ی observer آن انجام داد. همانطور که عنوان شد، تزئین کننده‌ها را می‌توان به صورت معمولی observer@ به یک کلاس و یا به صورت فراخوانی تابع، برای مثال به یک کامپوننت تابعی اعمال کرد. برای نمونه کامپوننت‌های کلاسی را به نحو زیر می‌توان با observer@ مزین کرد:
import { observer } from "mobx-react";

@observer class Counter extends Component {
در این حالت هر زمانیکه یکی از اشیاء observable تغییر می‌کند، React را وادار به رندر مجدد UI خواهد کرد.

و یا کامپوننت‌های تابعی را می‌توان توسط متد observer به صورت زیر محصور کرد:
const Counter = observer(({ count }) => {
  return (
   // ...
  );
});
با تحت نظر قرار گرفته شدن یک کامپوننت (چه با تزئین کننده‌ی observer@ و یا با بکارگیری نگارش تابعی آن)، منطقی که در پشت صحنه مورد استفاده قرار می‌گیرد، یک چنین شکلی را خواهد داشت (و برای اینکار نیازی به کد نویسی نیست):
class ContainerComponent extends Component () {
   componentDidMount() {
     this.stopListening = autorun(() => this.render());
   }

   componentWillUnmount() {
     this.stopListening();
   }

   render() { … }
}
زمانیکه کار رندر اولیه‌ی کامپوننت در DOM به پایان رسید، متد autorun به تغییرات observableها در پشت صحنه گوش‌فرا داده و سبب فراخوانی متد رندر کامپوننت، با هر تغییر لازمی می‌شود. این کاری است که متد یا تزئین کننده‌ی observer کتابخانه‌ی mobx-react انجام می‌دهد.

تعریف مخزن و اتصال آن به کامپوننت‌ها

کار شیء Provider که بالاترین کامپوننت را در سلسله مراتب کامپوننت‌ها محصور می‌کند، ارائه‌ی store، به تمام کامپوننت‌های فرزند است. در Redux فقط یک store را داریم که  به شیء Provider آن ارسال می‌کنیم. اما در حین کار با MobX چنین محدودیتی وجود ندارد و می‌توان چندین store را تعریف کرد و در اختیار برنامه قرار داد که شبه‌کد نحوه‌ی تعریف آن به صورت زیر است:
import { Provider } from 'mobx-react';

import ItemStore from './store/ItemStore';
import Application from './components/Application';

const itemStore = new ItemStore();

ReactDOM.render(
   <Provider itemStore={itemStore}>
     <Application />
   </Provider>,
   document.getElementById('root'),
);
در حین کار با Redux، قسمتی از مراحل تعریف آن، کار اتصال خواص موجود در state مخزن redux، به props یک کامپوننت است و یا همچنین کار اتصال رویدادها به props. یک چنین کاری را در اینجا به سادگی با تزئین کننده‌ای به نام inject می‌توان انجام داد که مخزن مورد استفاده را مشخص می‌کند:
@inject('itemStore')
class NewItem extends Component {
// ...
و یا برای کامپوننت‌های تابعی می‌توان از نگارش تابعی inject استفاده کرد. در این حالت، store تزریقی را می‌توان به صورت props دریافت نمود:
const UnpackedItems = inject('itemStore')(
    observer(({ itemStore }) => (
    // ...
  )),
);


یک مثال: پیاده سازی مثال شمارشگر قسمت سوم این سری با mobx-react

در ادامه قصد داریم برنامه‌ی شمارشگر ارائه شده در قسمت سوم بررسی redux را با mobx پیاده سازی کنیم. به همین جهت یک پروژه‌ی جدید React را ایجاد می‌کنیم:
> create-react-app state-management-with-mobx-part2
> cd state-management-with-mobx-part2
> npm start
در ادامه کتابخانه‌ها‌ی mobx ، mobx-react و همچنین بوت استرپ را نصب می‌کنیم. برای این منظور پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
> npm install --save mobx mobx-react bootstrap
سپس برای افزودن فایل bootstrap.css به پروژه‌ی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";

پس از آن فایل src\index.js را به صورت زیر تغییر می‌دهیم:
import "./index.css";
import "bootstrap/dist/css/bootstrap.css";

import { autorun, decorate, observable } from "mobx";
import React from "react";
import ReactDOM from "react-dom";

import Counter from "./components/Counter";
import * as serviceWorker from "./serviceWorker";

class Count {
  value = 0;

  increment = () => {
    this.value++;
  };

  decrement = () => {
    this.value--;
  };
}

decorate(Count, { value: observable });

const count = (window.count = new Count());
autorun(() => console.log("The count changed!", count.value));

ReactDOM.render(
  <main className="container">
    <Counter count={count} />
  </main>,
  document.getElementById("root")
);

serviceWorker.unregister();
توضیحات:
- در قسمت قبل، روش تحت نظر قرار دادن یک شیء متداول جاوا اسکریپتی را توسط متد observable مشاهده کردیم. در اینجا نگارش کلاسی آن مثال را بر اساس کلاس Count مشاهده می‌کنید. اگر نخواهیم از decorator ای مانند observable@ بر روی خاصیت value این کلاس استفاده کنیم، روش تابعی آن‌را با فراخوانی متد decorate و ذکر نوع کلاس و سپس خاصیتی که باید به صورت observable تحت نظر قرار گیرد، در اینجا مشاهده می‌کنید. این هم یک روش کار با mobx است.
- پس از ایجاد کلاس Count که در اینجا نقش store را نیز بازی می‌کند، یک وهله‌ی جدید را از آن ساخته و به متغیر count در این ماژول و همچنین window.count انتساب می‌دهیم. انتساب window.count سبب می‌شود تا بتوان در کنسول توسعه دهندگان مرورگر، با نوشتن count و سپس enter، به محتویات این متغیر دسترسی یافت و یا حتی آن‌را تغییر داد؛ مانند تصویر زیر که بلافاصله این تغییر، در UI برنامه نیز منعکس می‌شود:


- در اینجا تعریف شیء Provider را که پیشتر در مورد آن بحث کردیم، مشاهده نمی‌کنید؛ چون با تک کامپوننت Counter تعریف شده‌ی در این مثال، نیازی به آن نیست. می‌توان این شیء store را به صورت مستقیم به props کامپوننت Counter ارسال کرد.

اکنون تعریف کامپوننت شمارشگر واقع در فایل src\components\Counter.jsx به صورت زیر خواهد بود که این کامپوننت، count را به صورت props دریافت می‌کند:
import { observer } from "mobx-react";
import React from "react";

const Counter = observer(({ count }) => {
  return (
    <section className="card mt-5">
      <div className="card-body text-center">
        <span className="badge m-2 badge-primary">{count.value}</span>
      </div>
      <div className="card-footer">
        <div className="d-flex justify-content-center align-items-center">
          <button
            className="btn btn-secondary btn-sm"
            onClick={count.increment}
          >
            +
          </button>
          <button
            className="btn btn-secondary btn-sm m-2"
            onClick={count.decrement}
          >
            -
          </button>
        </div>
      </div>
    </section>
  );
});

export default Counter;
و سپس بر اساس count رسیده، در اینجا می‌توان مستقیما متدهای کلاس Count را فراخوانی کرد (مانند count.increment؛ که البته در اصل یک خاصیت است که با متدی مقدار دهی شده‌است) و یا مقدار خاصیتی از آن‌را مانند count.value، نمایش داد.
تا زمانیکه کامپوننت، با تابع observer محصور شده‌است، به props رسیده گوش فرا داده و خواص و اشیاء observable آن‌را تشخیص می‌دهد و سبب رندر مجدد کامپوننت، با تغییری در آن‌ها خواهد شد.

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part2.zip
مطالب
Globalization در ASP.NET MVC - قسمت ششم
در قسمت قبل ساختار اصلی و پیاده‌سازی ابتدایی یک پرووایدر سفارشی دیتابیسی شرح داده شد. در این قسمت ادامه بحث و مطالب پیشرفته‌تر آورده شده است.

تولید یک پرووایدر منابع دیتابیسی - بخش دوم
در بخش دوم این سری مطلب، ساختار دیتابیس و مباحث پیشرفته پیاده‌سازی کلاس‌های نشان داده‌شده در بخش اول در قسمت قبل شرح داده می‌شود. این مباحث شامل نحوه کش صحیح و بهینه داده‌های دریافتی از دیتابیس، پیاده‌سازی فرایند fallback، و پیاده‌سازی مناسب کلاس DbResourceManager برای مدیریت کل عملیات است.
 
ساختار دیتابیس
برای پیاده‌سازی منابع دیتابیسی روش‌های مختلفی برای آرایش جداول جهت ذخیره انواع ورودی‌ها می‌توان درنظر گرفت.
مثلا درصورتی‌که حجم و تعداد منابع بسیار باشد و نیز منابع دیتابیسی به اندازه کافی در دسترس باشد، می‌توان به ازای هر منبع یک جدول درنظر گرفت.
یا درصورتیکه منابع داده‌ای محدودتر باشند می‌توان به ازای هر کالچر یک جدول درنظر گرفت و تمام منابع مربوط به یک کالچر را درون یک جدول ذخیره کرد. درهرصورت نحوه انتخاب آرایش جداول منابع کاملا بستگی به شرایط کاری و سلایق برنامه‌نویسی دارد.
برای مطلب جاری به عنوان یک راه‌حل ساده و کارآمد برای پروژه‌های کوچک و متوسط، تمام ورودی‌های منابع درون یک جدول با ساختاری مانند زیر ذخیره می‌شود:


نام این جدول را با درنظر گرفتن شرایط موجود می‌توان Resources گذاشت.

ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.

ستون Key برای نگهداری کلید ورودی منبع استفاده می‌شود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره می‌شود. 

ستون Culture برای ذخیره کالچر ورودی منبع به کار می‌رود. این مقدار می‌تواند برای کالچر پیش‌فرض برنامه برابر رشته خالی باشد. 

ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده می‌شود. 

برای ستون Id می‌توان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.

نکته: برای امنیت بیشتر می‌توان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.

برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش می‌دهد دقت کنید:

 

اصلاح کلاس DbResourceProviderFactory

برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید به‌صورت زیر تغییر کند:

public override IResourceProvider CreateLocalResourceProvider(string virtualPath)
{
  if (!string.IsNullOrEmpty(virtualPath))
  {
    virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/'
  }
  return new LocalDbResourceProvider(virtualPath);
}
با این تغییر مسیرهای درخواستی چون "Default.aspx/~" و یا "Default.aspx/" هر دو به صورت "Default.aspx" در می‌آیند تا با نام ذخیره شده در دیتابیس یکسان شوند.
 

ارتباط با دیتابیس

خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راه‌های زیادی وجود دارد. برای پیاده‌سازی آن مثلا می‌توان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا به‌کارگیری IoC، نمونه مناسبی از پیاده‌سازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیاده‌سازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.

پس از پیاده‌سازی کلاس‌های مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با داده‌ها استفاده می‌شود که نمونه‌ای ابتدایی از آن در زیر آورده شده است:

using System.Collections.Generic;
using System.Linq;
using DbResourceProvider.Models;

namespace DbResourceProvider.Data
{
  public class ResourceData
  {
    private readonly string _resourceName;
    public ResourceData(string resourceName)
    {
      _resourceName = resourceName;
    }
    public Resource GetResource(string resourceKey, string culture)
    {
      using (var data = new TestContext())
      {
        return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture);
      }
    }
    public List<Resource> GetResources(string culture)
    {
      using (var data = new TestContext())
      {
        return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList();
      }
    }
  }
}
کلاس فوق نسبت به نمونه‌ای که در قسمت قبل نشان داده شد کمی فرق دارد. بدین صورت که برای راحتی بیشتر نام منبع درخواستی به جای پارامتر متدها، در اینجا به عنوان پارامتر کانستراکتور وارد می‌شود.

نکته: درصورتی‌که این کلاس‌ها در پروژه‌ای جداگانه قرار دارند، باید ConnectionString مربوطه در فایل کانفیگ برنامه مقصد نیز تنظیم شود.

کش کردن ورودی‌ها
برای کش کردن ورودی‌ها این نکته را که قبلا هم به آن اشاره شده بود باید درنظر داشت:
پس از اولین درخواست برای هر منبع، نمونه تولیدشده از پرووایدر مربوطه در حافظه سرور کش خواهد شد.
یعنی متدهای کلاس DbResourceProviderFactory به‌ازای هر منبع تنها یکبار فراخوانی می‌شود. نمونه‌های کش‌شده از پروایدرهای کلی و محلی به همراه تمام محتویاتشان (مثلا نمونه تولیدی از کلاس DbResourceManager) تا زمان Unload شدن سایت در حافظه سرور باقی می‌مانند. بنابراین عملیات کشینگ ورودی‌ها را می‌توان درون خود کلاس DbResourceManager به ازای هر منبع انجام داد.
برای کش کردن ورودی‌های هر منبع می‌توان چند روش را درپیش گرفت. روش اول این است که به ازای هر کلید درخواستی تنها ورودی مربوطه از دیتابیس فراخوانی شده و در برنامه کش شود. این روش برای حالاتی که تعداد ورودی‌ها یا تعداد درخواست‌های کلیدهای هر منبع کم باشد مناسب خواهد بود.
یکی از پیاده‌سازی این روش این است که ورودی‌ها به ازای هر کالچر ذخیره شوند. پیاده‌سازی اولیه این نوع فرایند کشینگ در کلاس DbResourceManager به صورت زیر است:
using System.Collections.Generic;
using System.Globalization;
using DbResourceProvider.Data;
namespace DbResourceProvider
{
  public class DbResourceManager
  {
    private readonly string _resourceName;
    private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture;
    public DbResourceManager(string resourceName)
    {
      _resourceName = resourceName;
      _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>();
    }
    public object GetObject(string resourceKey, CultureInfo culture)
    {
      return GetCachedObject(resourceKey, culture.Name);
    }
    private object GetCachedObject(string resourceKey, string cultureName)
    {
      if (!_resourceCacheByCulture.ContainsKey(cultureName))
        _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
      var cachedResource = _resourceCacheByCulture[cultureName];
      lock (this)
      {
        if (!cachedResource.ContainsKey(resourceKey))
        {
          var data = new ResourceData(_resourceName);
          var dbResource = data.GetResource(resourceKey, cultureName);
          if (dbResource == null) return null;
          var cachedResources = _resourceCacheByCulture[cultureName];
          cachedResources.Add(dbResource.Key, dbResource.Value);
        }
      }
      return cachedResource[resourceKey];
    }
  }
}
همانطور که قبلا توضیح داده شد کشِ پرووایدرهای منابع به ازای هر منبع درخواستی (و به تبع آن نمونه‌های موجود در آن مثل DbResourceManager) برعهده خود ASP.NET است. بنابراین برای کش کردن ورودی‌های درخواستی هر منبع در کلاس DbResourceManager تنها کافی است آن‌ها را درون یک متغیر محلی در سطح کلاس (فیلد) ذخیره کرد. کاری که در کد بالا در متغیر resourceCacheByCulture_ انجام شده است. در این متغیر که از نوع دیکشنری تعریف شده است کلیدهای هر عضو آن برابر نام کالچر مربوطه است. مقادیر هر عضو این دیکشنری نیز خود یک دیکشنری است که ورودی‌های منابع مربوط به کالچر مربوطه در آن ذخیره می‌شوند.
عملیات در متد GetCachedObject انجام می‌شود. همان‌طور که می‌بینید ابتدا وجود ورودی موردنظر در متغیر کشینگ بررسی می‌شود و درصورت عدم وجود، مقدار آن مستقیما از دیتابیس درخواست می‌شود. سپس این مقدار درخواستی ابتدا درون متغیر کشینگ ذخیره شده (به همراه بلاک lock) و درنهایت برگشت داده می‌شود.

نکته: کل فرایند بررسی وجود کلید در متغیر کشینگ (شرط دوم در متد GetCachedObject) درون بلاک lock قرار داده شده است تا در درخواست‌های همزمان احتمال افزودن چندباره یک کلید ازبین برود.

پیاده‌سازی دیگر این فرایند کشینگ، ذخیره ورودی‌ها براساس نام کلید به جای نام کالچر است. یعنی کلید دیکشنری اصلی نام کلید و کلید دیکشنری داخلی نام کالچر است که این روش زیاد جالب نیست.
روش دوم که بیشتر برای برنامه‌های بزرگ با ورودی‌ها و درخواست‌های زیاد به‌کار می‌رود این است که درهر بار درخواست به دیتابیس به جای دریافت تنها همان ورودی درخواستی، تمام ورودی‌های منبع و کالچر درخواستی استخراج شده و کش می‌شود تا تعداد درخواست‌های به سمت دیتابیس کاهش یابد. برای پیاده‌سازی این روش کافی است تغییرات زیر در متد GetCachedObject اعمال شود:
private object GetCachedObject(string resourceKey, string cultureName)
{
  lock (this)
  {
    if (!_resourceCacheByCulture.ContainsKey(cultureName))
    {
      _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>());
      var cachedResources = _resourceCacheByCulture[cultureName];
      var data = new ResourceData(_resourceName);
      var dbResources = data.GetResources(cultureName);
      foreach (var dbResource in dbResources)
      {
        cachedResources.Add(dbResource.Key, dbResource.Value);
      }
    }
  }
  var cachedResource = _resourceCacheByCulture[cultureName];
  return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey];
}
دراینجا هم می‌توان به جای استفاده از نام کالچر برای کلید دیکشنری اصلی از نام کلید ورودی منبع استفاده کرد که چندان توصیه نمی‌شود.

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

فرایند Fallback
درباره فرایند fallback به اندازه کافی در قسمت‌های قبلی توضیح داده شده است. برای پیاده‌سازی این فرایند ابتدا باید به نوعی به سلسله مراتب کالچرهای موجود از کالچر جاری تا کالچر اصلی و پیش فرض سیستم دسترسی پیدا کرد. برای اینکار ابتدا باید با استفاده از روشی کالچر والد یک کالچر را بدست آورد. کالچر والد کالچری است که عمومیت بیشتری نسبت به کالچر موردنظر دارد. مثلا کالچر fa، کالچر والد fa-IR است. همچنین کالچر Invariant به عنوان والد تمام کالچرها شناخته می‌شود.
خوشبختانه در کلاس CultureInfo (که در قسمت‌های قبلی شرح داده شده است) یک پراپرتی با عنوان Parent وجود دارد که کالچر والد را برمی‌گرداند.
برای رسیدن به سلسله مراتب مذبور در کلاس ResourceManager دات نت، از کلاسی با عنوان ResourceFallbackManager استفاده می‌شود. هرچند این کلاس با سطح دسترسی internal تعریف شده است اما نام‌گذاری نامناسبی دارد زیرا کاری که می‌کند به عنوان Manager هیچ ربطی ندارد. این کلاس با استفاده از یک کالچر ورودی، یک enumerator از سلسله مراتب کالچرها که در بالا صحبت شد تهیه می‌کند.
با استفاده پیاده‌سازی موجود در کلاس ResourceFallbackManager کلاسی با عنوان CultureFallbackProvider تهیه کردم که به صورت زیر است:
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
namespace DbResourceProvider
{
  public class CultureFallbackProvider : IEnumerable<CultureInfo>
  {
    private readonly CultureInfo _startingCulture;
    private readonly CultureInfo _neutralCulture;
    private readonly bool _tryParentCulture;
    public CultureFallbackProvider(CultureInfo startingCulture = null, 
                                   CultureInfo neutralCulture = null, 
                                   bool tryParentCulture = true)
    {
      _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture;
      _neutralCulture = neutralCulture;
      _tryParentCulture = tryParentCulture;
    }
    #region Implementation of IEnumerable<CultureInfo>
    public IEnumerator<CultureInfo> GetEnumerator()
    {
      var reachedNeutralCulture = false;
      var currentCulture = _startingCulture;
      do
      {
        if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name)
        {
          yield return CultureInfo.InvariantCulture;
          reachedNeutralCulture = true;
          break;
        }
        yield return currentCulture;
        currentCulture = currentCulture.Parent;
      } while (_tryParentCulture && !HasInvariantCultureName(currentCulture));
      if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture)
        yield break;
      yield return CultureInfo.InvariantCulture;
    }
    #endregion
    #region Implementation of IEnumerable
    IEnumerator IEnumerable.GetEnumerator()
    {
      return GetEnumerator();
    }
    #endregion
    private bool HasInvariantCultureName(CultureInfo culture)
    {
      return culture.Name == CultureInfo.InvariantCulture.Name;
    }
  }
}
این کلاس که اینترفیس <IEnumerable<CultureInfo را پیاده‎سازی کرده است، سه پارامتر کانستراکتور دارد.
اولین پارامتر، کالچر جاری یا آغازین را مشخص می‌کند. این کالچری است که تولید enumerator مربوطه از آن آغاز می‌شود. درصورتی‌که این پارامتر نال باشد مقدار کالچر UI در ثرد جاری برای آن درنظر گرفته می‌شود. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
پارامتر بعدی کالچر خنثی موردنظر کاربر است. این کالچری است که درصورت رسیدن enumerator به آن کار پایان خواهد یافت. درواقع کالچر پایانی enumerator است. این پارامتر می‌تواند نال باشد. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، null است.
پارمتر آخر هم تعیین می‌کند که آیا enumerator از کالچرهای والد استفاده بکند یا خیر. مقدار پیش‌فرضی که برای این پارامتر درنظر گرفته شده است، true است. 
کار اصلی کلاس فوق در متد GetEnumerator انجام می‌شود. در این کلاس یک حلقه do-while وجود دارد که enumerator را با استفاده از کلمه کلیدی yield تولید می‌کند. در این متد ابتدا درصورت نال نبودن کالچر خنثی ورودی، بررسی می‌شود که آیا نام کالچر جاری حلقه (که در متغیر محلی currentCulture ذخیره شده است) برابر نام کالچر خنثی است یا خیر. درصورت برقراری شرط، کار این حلقه با برگشت CultureInfo.InvariantCulture پایان می‌‌یابد. InvariantCulture کالچر بدون زبان و فرهنگ و موقعیت مکانی است که درواقع به عنوان کالچر والد تمام کالچرها درنظر گرفته می‌شود. پراپرتی Name این کالچر برابر string.Empty است.
کار حلقه با برگشت مقدار کالچر جاری enumerator ادامه می‌یابد. سپس کالچر جاری با کالچر والدش مقداردهی می‌شود. شرط قسمت while حلقه تعیین می‌کند که درصورتی‌که کلاس برای استفاده از کالچرهای والد تنظیم شده باشد، تا زمانی که نام کالچر جاری برابر نام کالچر Invariant نباشد، تولید اعضای enumerator ادامه یابد.
درانتها نیز درصورتی‌که با شرایط موجود، قبلا کالچر Invariant برگشت داده نشده باشد این کالچر نیز yield می‌شود. درواقع درصورتی‌که استفاده از کالچرهای والد اجازه داده نشده باشد یا کالچر آغازین برابر کالچر Invariant باشد و یا قبلا به دلیل رسیدن به کالچر خنثی ورودی، مقدار کالچر Invariant برگشت داده شده باشد، enumerator قطع شده و عملیات پایان می‌یابد. در غیر اینصورت کالچر Invariant به عنوان کالچر پایانی برگشت داده می‌شود.
 
استفاده از CultureFallbackProvider
با استفاده از کلاس CultureFallbackProvider می‌توان عملیات جستجوی ورودی‌های درخواستی را با ترتیبی مناسب بین تمام کالچرهای موجود به انجام رسانید.
برای استفاده از این کلاس باید تغییراتی در متد GetObject کلاس DbResourceManager به صورت زیر اعمال کرد:
public object GetObject(string resourceKey, CultureInfo culture)
{
  foreach (var currentCulture in new CultureFallbackProvider(culture))
  {
    var value = GetCachedObject(resourceKey, currentCulture.Name);
    if (value != null) return value;
  }
  throw new KeyNotFoundException("The specified 'resourceKey' not found.");
}
با استفاده از یک حلقه foreach درون enumerator کلاس CultureFallbackProvider، کالچرهای موردنیاز برای fallback یافته می‌شوند. در اینجا از مقادیر پیش‌فرض دو پارامتر دیگر کانستراکتور کلاس CultureFallbackProvider استفاده شده است.
سپس به ازای هر کالچر یافته شده مقدار ورودی درخواستی بدست آمده و درصورتی‌که نال نباشد (یعنی ورودی موردنظر برای کالچر جاری یافته شود) آن مقدار برگشت داده می‌شود و درصورتی‌که نال باشد عملیات برای کالچر بعدی ادامه می‌یابد.
درصورتی‌که ورودی درخواستی یافته نشود (خروج از حلقه بدون برگشت مقداری برای ورودی منبع درخواستی) استثنای KeyNotFoundException صادر می‌شود تا کاربر را از اشتباه رخداده مطلع سازد.

آزمایش پرووایدر سفارشی

ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.

داده‌های نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:

<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" />
<asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
خروجی برای کالچر "en-US" (معمولا پیش‌فرض، اگر تنظیمات سیستم عامل تغییر نکرده باشد) چیزی شبیه تصویر زیر خواهد بود:

سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):

<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
بنابراین صفحه Default.aspx با همان داده‌های نشان داده شده در بالا به صورت زیر تغییر خواهد کرد:

می‌بینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.

بحث و نتیجه‎‌گیری

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

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

در ادامه درباره روش‌های مختلف نحوه پیاده‌سازی قابلیت به‌روزرسانی ورودی‌های منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راه‌حل‌های مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژه‌های MVC شرح داده می‌شود.

البته مباحث پیشرفته‌تری چون تزریق وابستگی برای پیاده‌سازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.

منابع

http://weblogs.asp.net/thangchung/archive/2010/06/25/extending-resource-provider-for-soring-resources-in-the-database.aspx

http://msdn.microsoft.com/en-us/library/aa905797.aspx

http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx

http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs

http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider

http://www.west-wind.com/presentations/wwdbresourceprovider

مطالب
برنامه نویسی پیشرفته JavaScript - قسمت 2 - ماهیت انواع داده‌ای و حوزه دسترسی به متغیرها

مقادیر پایه (Primitive Values) و ارجاعی (Reference Values)

در جاوا اسکریپت، متغیرها شامل داده‌هایی از نوع پایه و یا ارجاعی می‌باشند. مقادیر String ، Number ، Boolean ، Null و Undefined به عنوان مقادیر پایه محسوب می‌گردند. در این نوع متغیرها تغییرات، مستقیما بر روی مقدار ذخیره شده در متغیر اعمال می‌شوند. اشیاء نیز که از مجموعه‌ای از مقادیر پایه ساخته شده‌اند، مقادیر ارجاعی نامیده می‌شوند. در این نوع مقادیر، شما به اشاره‌گری از شیء دسترسی دارید. بنابراین تغییرات مستقیما بر روی داده‌های ذخیره شده اعمال نمی‌شوند. به مثال‌های زیر توجه کنید:

var num1 = 10;
var num2 = num1;
alert("Num1=" + num1 + ", Num2=" + num2);

num2 = 20;
alert("Num1=" + num1 + ", Num2=" + num2);

num1 = 30;
alert("Num1=" + num1 + ", Num2=" + num2);

خروجی :

"Num1=10, Num2=10"

"Num1=10, Num2=20"

"Num1=30, Num2=20"


همانطور که از خروجی مثال فوق پیداست، در انتساب num1 به num2 ، مقدار num1 در num2 کپی شده‌است. بنابراین تغییراتی که بر روی num1 یا num2 صورت می‌گیرد، مستقیما بر روی مقدار ذخیره شده در هر یک از این متغیرها تاثیر می‌گذارد. رفتار مقادیر پایه همیشه به همین صورت می‌باشد.

var obj1 = new Object();
obj1.num = 10;
var obj2 = obj1;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

obj2.num = 20;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

obj1.num = 30;
alert("Obj1.Num=" + obj1.num + ", Obj2.Num=" + obj2.num);

خروجی :

"Obj1.Num=10, Obj2.Num=10"

"Obj1.Num=20, Obj2.Num=20"

"Obj1.Num=30, Obj2.Num=30"


با استفاده از نوع ارجاعی Object می‌توانیم اشیاء جدیدی را ایجاد کنیم و ویژگی‌هایی را به صورت پویا به آن‌ها اختصاص دهیم. همانطور که قبلا گفته شد، اشیاء از نوع ارجاعی می‌باشند و حاوی اشاره‌گری به مقادیر ذخیره شده می‌باشند. بنابراین انتساب obj1 به obj2 به معنای انتساب اشاره‌گر obj1 به obj2 می‌باشد. به عبارتی دیگر obj2 به همانجایی اشاره می‌کند که obj1 نیز اشاره می‌نماید. پس هر تغییری که بر روی ویژگی‌های obj1 رخ دهد، obj2 نیز تاثیرات آن را می‌بیند و بالعکس. همانطور که در خروجی مشاهده می‌نمایید، در مرحله‌ی اول obj1 به obj2 نسبت داده شد، پس مقدار ویژگی num برای هر دو آنها یکسان میباشد. در مرحله‌ی دوم، مقدار ویژگی num را در obj2 تغییر دادیم؛ ولی مقدار این ویژگی، در obj1 نیز تغییر نمود. در مرحله‌ی سوم نیز همان اتفاقات مرحله‌ی دوم تکرار شد.

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

توجه:

به انواع پایه، انواع داده‌ای مقداری یا اولیه نیز گفته می‌شود.


فراخوانی با مقدار (Call by Value)

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

function primitive(a, b) {
a += 100;
b += 200;
alert("a=" + a + ", b=" + b);
}

var x = 300, y = 400;
primitive(x, y);
alert("x=" + x + ", y=" + y);

خروجی :

"a=400, b=600"

"x=300, y=400"


x و y دو متغیر پایه می‌باشند، بنابراین تابع فوق به صورت مقداری فراخوانی شده‌است. یعنی مقدار آرگومان‌های x و y در آرگومان‌های a و b کپی می‌شوند. پس هر تغییری که بر روی a و b رخ دهد، هیچ تاثیری بر روی x و y ندارد. همچنین با توجه به توضیحی که در مورد مقادیر پایه داده شد، تغییرات مستقیما بر روی داده‌ی ذخیره شده در متغیر اعمال میشود. بنابراین تغییراتی که در تابع فوق بر روی a و b رخ داد، مستقیما مقادیر a و b را تغییر داده‌است وهیچ ارتباطی به x و y ندارد. البته توجه داشته باشید که حتی اگر نام آرگومان‌های تابع با آرگومان‌های ارسالی یکسان بود و یا حتی اگر تابع مقداری را به عنوان خروجی بر می‌گرداند، هیچ تفاوتی را در خروجی برنامه فوق مشاهده نمی‌کردید.


فراخوانی با ارجاع (Call by Reference)

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

function reference(obj) {
obj.a += 100;
obj.b += 200;
alert("obj.a=" + obj.a + ", obj.b=" + obj.b);
}

var calc = new Object();
calc.a = 300;
calc.b = 400;

reference(calc);
alert("calc.a=" + calc.a + ", calc.b=" + calc.b);

خروجی :

"obj.a=400, obj.b=600"

"calc.a=400, calc.b=600"


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


حوزه دسترسی به متغیرها (Variable Scope)

متغیر‌های محلی (Local Variables)

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

for (var i = 1; i <= 5; i++) {
var sqr = i * i;
alert(sqr);
}

alert(i);
alert(sqr);

خروجی :

1
4
9
16
25
6   // alert(i) out of for
25   // alert(sqr) out of for


متغیرهای i و sqr داخل حلقه‌ی for تعریف شده‌اند و منطقا نباید خارج از این حلقه قابل دسترسی باشند. ولی با توجه به خروجی فوق، مشاهده نمودید که متغیرهای i و sqr، نه تنها خارج از این حلقه قابل شناسایی می‌باشند، بلکه آخرین مقدار خود را نیز حفظ نموده‌اند. در جاوا اسکریپت، یک متغیر محلی زمانی مفهوم پیدا می‌کند که در داخل یک تابع تعریف شود. به مثال زیر توجه کنید:

function sqr(num) {
var sum = num * num;
return sum;
}

var n = 4;
alert(sqr(n));
alert(num); // Error: num is not defined
alert(sum); // Error: sum is not defined

خروجی :

16

Error: num is not defined

Error: sum is not defined

همانطور که مشاهده می‌کنید، متغیرهای num و sum به صورت محلی در تابع فوق تعریف شده‌اند؛ بنابراین خارج از تابع قابل دسترسی نمی‌باشند و موجب بروز خطا می‌گردند.


متغیرهای عمومی (Global Variables)

در جاوا اسکریپت، متغیرهایی که خارج از تابع تعریف می‌گردند، به شیء window نسبت داده می‌شوند و عمومی می‌باشند. به عبارتی دیگر این متغیرها به عنوان یک ویژگی از شیء window تعریف می‌شوند و در تمامی توابع و بلاک‌های کد قابل دسترسی می‌باشند. به مثال زیر توجه کنید:

var color = "Red";

function setColor() {
color = "Blue";
}

alert(color);
setColor();
alert(color);

خروجی :

"Red"

"Blue"


در مثال فوق، متغیر color به صورت عمومی تعریف شده‌است. بنابراین در کل برنامه قابل دسترسی می‌باشد. در alert اول مقدار فعلی متغیر color یعنی “Red” نمایش می‌یابد. سپس با فراخوانی تابع، مقدار این متغیر تغییر می‌کند. در alert دوم مقدار تغییر یافته‌ی متغیر color نمایش خواهد یافت. حال به مثال زیر توجه کنید:

var color = "Red";

function getColor() {
var color = "Blue";
return color;
}

alert(color);
alert(getColor());
alert(color);

خروجی :

"Red"

"Blue"

"Red"


در مثال فوق، ابتدا یک متغیر color به صورت عمومی یا Global تعریف شده است. در تابع getColor نیز یک متغیر color به صورت local یا محلی تعریف شده است. زمانی که در alert تابع getColor فراخوانی می‌شود، متغیر color مقداردهی می‌گردد. این مقداردهی برای متغیر محلی صورت گرفته است و هیچ ربطی به متغیر color که به صورت عمومی تعریف شده است ندارد.

جهت تعریف متغیر در جاوا اسکریپت، از کلمه‌ی کلیدی var استفاده می‌شود. اما تعریف متغیر در جاوا اسکریپت اجباری نمی‌باشد و می‌توان یک متغیر را مقداردهی نمود بدون آنکه تعریف شده باشد. در صورتی که متغیر با var اعلان نشود، آن متغیر به شیء window نسبت داده می‌شود و ماهیت عمومی پیدا می‌کند. به مثال زیر توجه کنید:

function sum(a, b) {
c = a + b;
}

sum(20, 30);
alert(c);

خروجی :

50


همانطور که مشاهده می‌کنید، متغیر c بدون تعریف شدن مورد استفاده قرار گرفته است. با اینکه به صورت محلی مقداردهی گردیده است، ولی چون توسط var اعلان نشده است، به شیء window نسبت داده شده و ماهیت عمومی پیدا کرده است. 

مطالب دوره‌ها
بررسی کارآیی و ایندکس گذاری بر روی اسناد XML در SQL Server - قسمت اول
در ادامه‌ی مباحث پشتیبانی از XML در SQL Server، به کارآیی فیلدهای XML ایی و نحوه‌ی ایندکس گذاری بر روی آن‌ها خواهیم پرداخت. این مساله در تولید برنامه‌هایی سریع و مقیاس پذیر، بسیار حائز اهمیت است.
در SQL Server، کوئری‌های انجام شده بر روی فیلدهای XML، توسط همان پردازشگر کوئری‌های رابطه‌ای متداول آن، خوانده و اجرا خواهند شد و امکان تعریف یک XQuery خارج از یک عبارت SQL و یا T-SQL وجود ندارد. متدهای XQuery بسیار شبیه به system defined functions بوده و Query Plan یکپارچه‌ای را با سایر قسمت‌های رابطه‌ای یک عبارت SQL دارند.


مفهوم Node table

داده‌های XML ایی برای اینکه توسط SQL Server قابل استفاده باشند، به صورت درونی تبدیل به یک node table می‌شوند. به این معنا که نودهای یک سند XML، به یک جدول رابطه‌ای به صورت خودکار تجزیه می‌شوند. این جدول درونی در صورت بکارگیری XML Indexes در جدول سیستمی sys.internal_tables قابل مشاهده خواهد بود. SQL Server برای انجام اینکار از یک XmlReader خاص خودش استفاده می‌کند. در مورد XMLهای ایندکس نشده، این تجزیه در زمان اجرا صورت می‌گیرد؛ پس از اینکه Query Plan آن تشکیل شد.


بررسی Query Plan فیلدهای XML ایی

جهت فراهم کردن مقدمات آزمایش، ابتدا جدول xmlInvoice را با یک فیلد XML ایی untyped درنظر بگیرید:
 CREATE TABLE xmlInvoice
(
 invoiceId INT IDENTITY PRIMARY KEY,
 invoice XML
)
سپس 6 ردیف را به آن اضافه می‌کنیم:
INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1000" dept="hardware">
<CustomerName>Vahid</CustomerName>
<LineItems>
<LineItem><Description>Gear</Description><Price>9.5</Price></LineItem>
</LineItems>
</Invoice>
 ')

INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1002" dept="garden">
<CustomerName>Mehdi</CustomerName>
<LineItems>
<LineItem><Description>Shovel</Description><Price>19.2</Price></LineItem>
</LineItems>
</Invoice>
 ')

INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1003" dept="garden">
<CustomerName>Mohsen</CustomerName>
<LineItems>
<LineItem><Description>Trellis</Description><Price>8.5</Price></LineItem>
</LineItems>
</Invoice>
 ')

INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1004" dept="hardware">
<CustomerName>Hamid</CustomerName>
<LineItems>
<LineItem><Description>Pen</Description><Price>1.5</Price></LineItem>
</LineItems>
</Invoice>
 ')

INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1005" dept="IT">
<CustomerName>Ali</CustomerName>
<LineItems>
<LineItem><Description>Book</Description><Price>3.2</Price></LineItem>
</LineItems>
</Invoice>
 ')

INSERT INTO xmlInvoice 
VALUES('
<Invoice InvoiceId="1006" dept="hardware">
<CustomerName>Reza</CustomerName>
<LineItems>
<LineItem><Description>M.Board</Description><Price>19.5</Price></LineItem>
</LineItems>
</Invoice>
 ')
همچنین برای مقایسه، دقیقا جدول مشابهی را اینبار با یک XML Schema مشخص ایجاد می‌کنیم.
CREATE XML SCHEMA COLLECTION invoice_xsd AS
 ' <xs:schema attributeFormDefault="unqualified" 
 elementFormDefault="qualified" 
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="Invoice">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="CustomerName" type="xs:string" />
        <xs:element name="LineItems">
          <xs:complexType>
            <xs:sequence>
              <xs:element name="LineItem">
                <xs:complexType>
                  <xs:sequence>
                    <xs:element name="Description" type="xs:string" />
                    <xs:element name="Price" type="xs:decimal" />
                  </xs:sequence>
                </xs:complexType>
              </xs:element>
            </xs:sequence>
          </xs:complexType>
        </xs:element>
      </xs:sequence>
      <xs:attribute name="InvoiceId" type="xs:unsignedShort" use="required" />
      <xs:attribute name="dept" type="xs:string" use="required" />
    </xs:complexType>
  </xs:element>
</xs:schema>'

Go

CREATE TABLE xmlInvoice2
(
invoiceId INT IDENTITY PRIMARY KEY,
invoice XML(document invoice_xsd)
)
Go
سپس مجددا همان 6 رکورد قبلی را در این جدول جدید نیز insert خواهیم کرد.
در این جدول دوم، حالت پیش فرض content قبلی، به document تغییر کرده‌است. با توجه به اینکه می‌دانیم اسناد ما چه فرمتی دارند و بیش از یک root element نخواهیم داشت، انتخاب document سبب خواهد شد تا Query Plan بهتری حاصل شود.

در ادامه برای مشاهده‌ی بهتر نتایج، کش Query Plan و اطلاعات آماری جدول xmlInvoice را حذف و به روز می‌کنیم:
 UPDATE STATISTICS xmlInvoice
DBCC FREEPROCCACHE
به علاوه در management studio بهتر است از منوی Query، گزینه‌ی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمه‌های Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده نمود. برای خواندن یک Query Plan عموما از بالا به پایین و از راست به چپ باید عمل کرد. در آن نهایتا باید به عدد estimated subtree cost کوئری، دقت داشت.

کوئری‌هایی را که در این قسمت بررسی خواهیم کرد، در ادامه ملاحظه می‌کنید. بار اول این کوئری‌ها را بر روی xmlInvoice و بار دوم، بر روی نگارش دوم دارای اسکیمای آن اجرا خواهیم کرد:
 -- query 1
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice[@InvoiceId = "1003"]') = 1

-- query 2
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice/@InvoiceId[. = "1003"]') = 1

-- query 3
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice[1]/@InvoiceId[. = "1003"]') = 1

-- query 4
SELECT * FROM xmlInvoice
WHERE invoice.exist('(/Invoice/@InvoiceId)[1][. = "1003"]') = 1

-- query 5
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice[CustomerName = "Vahid"]') = 1

-- query 6
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice/CustomerName [.= "Vahid"]') = 1

-- query 7
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice/LineItems/LineItem[Description = "Trellis"]') = 1

-- query 8
SELECT * FROM xmlInvoice
WHERE invoice.exist('/Invoice/LineItems/LineItem/Description [.= "Trellis"]') = 1

-- query 9
SELECT * FROM xmlInvoice
WHERE invoice.exist('
for $x in /Invoice/@InvoiceId
where $x = 1003
return $x
') = 1

-- query 10
SELECT * FROM xmlInvoice
WHERE invoice.value('(/Invoice/@InvoiceId)[1]', 'VARCHAR(10)') = '1003'


-- یکبار هم با جدول شماره 2 که اسکیما دارد تمام این موارد تکرار شود

UPDATE STATISTICS xmlInvoice
DBCC FREEPROCCACHE

GO

کوئری 1

همانطور که عنوان شد، از منوی Query گزینه‌ی Include actual execution plan را نیز انتخاب کنید (یا فشردن دکمه‌های Ctrl+M) تا پس از اجرای کوئری، بتوان Query Plan نهایی را نیز مشاهده کرد.
در کوئری 1، با استفاده از متد exist به دنبال رکوردهایی هستیم که دارای ویژگی InvoiceId مساوی 1003 هستند. پس از اجرای کوئری، تصویر Query Plan آن به شکل زیر خواهد بود:


برای خواندن این تصویر، از بالا به پایین و چپ به راست باید عمل شود. هزینه‌ی انجام کوئری را نیز با نگه داشتن کرسر ماوس بر روی select نهایی سمت چپ تصویر می‌توان مشاهده کرد. البته باید درنظر داشت که این اعداد از دیدگاه Query Processor مفهوم پیدا می‌کنند. پردازشگر کوئری، بر اساس اطلاعاتی که در اختیار دارد، سعی می‌کند بهترین روش پردازش کوئری دریافتی را پیدا کند. برای اندازه گیری کارآیی، باید اندازه گیری زمان اجرای کوئری، مستقلا انجام شود.


در این کوئری، مطابق تصویر اول، ابتدا قسمت SQL آن (چپ بالای تصویر) پردازش می‌شود و سپس قسمت XML آن. قسمت XQuery این عبارت در دو قسمت سمت چپ، پایین تصویر مشخص شده‌اند. Table valued functionها جاهایی هستند که node table ابتدای بحث جاری در آن‌ها ساخته می‌شوند. در اینجا دو مرحله‌ی تولید Table valued functionها مشاهده می‌شود. اگر به جمع درصدهای آن‌ها دقت کنید، هزینه‌ی این دو قسمت، 98 درصد کل Query plan است.
سؤال: چرا دو مرحله‌ی تولید Table valued functionها در اینجا قابل مشاهده است؟ یک مرحله‌ی آن مربوط است به انتخاب نود Invoice و مرحله‌ی دوم مربوط است به فیلتر داخل [] ذکر شد برای یافتن ویژگی‌های مساوی 1003.

در اینجا و در کوئری‌های بعدی، هر Query Plan ایی که تعداد مراحل تولید Table valued function کمتری داشته باشد، بهینه‌تر است.


کوئری 5

اگر کوئری پلن شماره 5 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید. یک XML Reader برای خارج از [] (اصطلاحا به آن predicate گفته می‌شود) و دو مورد برای داخل [] تشکیل شده‌است؛ یکی برای انتخاب نود متنی و دیگری برای تساوی.

کوئری 7

اگر کوئری پلن شماره 7 را بررسی کنیم، به 3 مرحله تولید Table valued functionها خواهیم رسید که بسیار شبیه است به مورد 5. بنابراین در اینجا عمق بررسی و سلسله مراتب اهمیتی ندارد.

کوئری 9

کوئری 9 دقیقا معادل است با کوئری 1 نوشته شده؛ با این تفاوت که از روش FLOWR استفاده کرده‌است. نکته‌ی جالب آن، وجود تنها یک XML reader در Query plan آن است که باید آن‌را بخاطر داشت.


کوئری 2
کوئری 3
کوئری 4
کوئری 6
کوئری 8

اگر به این 5 کوئری یاد شده دقت کنید، از یک دات به معنای self استفاده کرده‌اند (یعنی پردازش بیشتری را انجام نده و از همین نود جاری برای پردازش نهایی استفاده کن). با توجه به بکارگیری متد exist، معنای کوئری‌های یک و دو، یکی‌است. اما در کوئری شماره 2، تنها یک XML Reader در Query plan نهایی وجود دارد (همانند عبارت FLOWR کوئری شماره 9).

یک نکته: اگر می‌خواهید بدانید بین کوئری‌های 1 و 2 کدامیک بهتر عمل می‌کنند، از بین تمام کوئری‌های موجود، دو کوئری یاد شده را انتخاب کرده و سپس با فرض روش بودن نمایش Query plan، هر دو کوئری را با هم اجرا کنید.


در این حالت، کوئری پلن‌های هر دو کوئری را با هم یکجا می‌توان مشاهده کرد؛ به علاوه‌ی هزینه‌ی نسبی آن‌ها را در کل عملیات صورت گرفته. در حالت استفاده از دات و وجود تنها یک XML Reader، این هزینه تنها 6 درصد است، در مقابل هزینه‌ی 94 درصدی کوئری شماره یک.
بنابراین از دیدگاه پردازشگر کوئری‌های SQL Server، کوئری شماره 2، بسیار بهتر است از کوئری شماره 1.

در کوئری‌های 3 و 4، شماره نود مدنظر را دقیقا مشخص کرده‌ایم. این مورد در حالت سوم تفاوت محسوسی را از لحاظ کارآیی ایجاد نمی‌کند و حتی کارآیی را به علت اضافه کردن یک XML Reader دیگر برای پردازش عدد نود وارد شده، کاهش می‌دهد. اما کوئری 4 که عدد اولین نود را خارج از پرانتز قرار داده‌است، تنها در کل یک XML Reader را به همراه خواهد داشت.

سؤال: بین کوئری‌های 2، 3 و 4 کدامیک بهینه‌تر است؟


بله. اگر هر سه کوئری را با هم انتخاب کرده و اجرا کنیم، می‌توان در قسمت کوئری پلن‌ها، هزینه‌ی هر کدام را نسبت به کل مشاهده کرد. در این حالت کوئری 4 بهتر است از کوئری 2 و تنها یک درصد هزینه‌ی کل را تشکیل می‌دهد.

کوئری 10

کوئری 10 اندکی متفاوت است نسبت به کوئری‌های دیگر. در اینجا بجای متد exist از متد value استفاده شده‌است. یعنی ابتدا صریحا  مقدار ویژگی InvoiceId استخراج شده و با 1003 مقایسه می‌شود.
اگر کوئری پلن آن‌را با کوئری 4 که بهترین کوئری سری exist است مقایسه کنیم، کوئری 10، هزینه‌ی 70 درصدی کل عملیات را به خود اختصاص خواهد داد، در مقابل 30 درصد هزینه‌ی کوئری 4. بنابراین در این موارد، استفاده از متد exist بسیار بهینه‌تر است از متد value.



استفاده از Schema collection و تاثیر آن بر کارآیی

تمام مراحلی را که در اینجا ملاحظه کردید، صرفا با تغییر نام xmlInvoice به xmlInvoice2، تکرار کنید. xmlInvoice2 دارای ساختاری مشخص است، به همراه ذکر صریح document حین تعریف ستون XML ایی آن.
تمام پاسخ‌هایی را که دریافت خواهید کرد با حالت بدون Schema collection یکی است.
برای مقایسه بهتر، یکبار نیز سعی کنید کوئری 1 جدول xmlInvoice را با کوئری 1 جدول xmlInvoice2 با هم در طی یک اجرا مقایسه کنید، تا بهتر بتوان Query plan نسبی آن‌ها را بررسی کرد.
پس از این بررسی و مقایسه، به این نتیجه خواهید رسید که تفاوت محسوسی در اینجا و بین این دو حالت، قابل ملاحظه نیست. در SQL Server از Schema collection بیشتر برای اعتبارسنجی ورودی‌ها استفاده می‌شود تا بهبود کارآیی کوئری‌ها.


بنابراین به صورت خلاصه
- متد exist را به value ترجیح دهید.
- اصطلاحا ordinal (همان مشخص کردن نود 1 در اینجا) را در آخر قرار دهید (نه در بین نودها).
- مراحل اجرایی را با معرفی دات (استفاده از نود جاری) تا حد ممکن کاهش دهید.

و ... کوئری 4 در این سری، بهترین کارآیی را ارائه می‌دهد.
مطالب
تنظیمات و نکات کاربردی کتابخانه‌ی 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 اضافه شده‌است تا توسط آن بتواند ارجاعات و نمونه‌های اشیاء را تشخیص دهد.
مطالب
مدیریت سراسری خطاها در یک برنامه‌ی Angular
در این مطلب قصد داریم پیام‌ها و اخطارهای برنامه را توسط کامپوننت Angular2 Toasty نمایش داده و همچنین برای کاهش میزان تکرار قسمت‌های نمایش خطا در برنامه، کار مدیریت متمرکز و سراسری آن‌ها را نیز انجام دهیم.


نمایش پیام‌ها و اخطارهای یک برنامه‌ی Angular توسط ng2-toasty

در مطلب «ایجاد Drop Down List‌های آبشاری در Angular» در قسمت دریافت اطلاعات drop down دوم از سرور، اگر کاربر مجددا گروه را بر روی حالت «لطفا گروهی را انتخاب کنید ...» قرار دهد، مقدار categoryId به undefined تغییر می‌کند:
  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      return;
    }
در اینجا می‌خواهیم توسط کامپوننت Angular2 Toasty، پیام متناسبی را نمایش دهیم:



پیشنیازهای کار با کامپوننت Angular2 Toasty توسط یک برنامه‌ی Angular CLI

برای کار با کامپوننت Angular2 Toasty، ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده و سپس دستور ذیل را صادر می‌کنیم:
> npm install ng2-toasty --save
اینکار سبب خواهد شد تا این کامپوننت در پوشه‌ی node_modules\ng2-toasty نصب شده و همچنین فایل package.json نیز جهت درج مدخل آن به روز رسانی شود:


یک نکته: اگر در حین اجرای این دستور به خطای ذیل برخوردید:
 npm ERR! Error: EPERM: operation not permitted, rename
چون VSCode پوشه‌ی node_modules را تحت نظر قرار می‌دهد، ممکن است یک سری اعمال npm مجوز اجرا را پیدا نکنند. بنابراین ابتدا VSCode را بسته و مجددا دستور npm را اجرا کنید.

پس از آن نیاز است یکی از شیوه‌نامه‌هایی را که در تصویر فوق ملاحظه می‌کنید، در فایل angular-cli.json. مشخص کنیم:
"styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
    "../node_modules/ng2-toasty/bundles/style-bootstrap.css",
    "styles.css"
],
که برای نمونه در اینجا، شیوه‌نامه‌ی بوت استرپ آن انتخاب شده‌است.

سپس باید به فایل src\app\app.module.ts مراجعه کرد و ماژول این کامپوننت را معرفی نمود:
import { ToastyModule } from "ng2-toasty";

@NgModule({
  imports: [
    BrowserModule,
    ToastyModule.forRoot(),

همچنین در همین قسمت، به فایل قالب src\app\app.component.html مراجعه کرده و selector tag این کامپوننت را در ابتدای آن تعریف می‌کنیم:
 <ng2-toasty [position]="'top-right'"></ng2-toasty>
در اینجا با استفاده از property binding و تعیین مقدار رشته‌ای top-right، محل نمایش اعلانات برنامه را مشخص می‌کنیم. مقدارهای ممکن آن شامل bottom-right، bottom-left، top-right، top-left، top-center، bottom-center، center-center هستند. برای مثال اگر می‌خواهید آن‌را در میانه‌ی صفحه نمایش دهید، مقدار center-center را انتخاب کنید. همچنین باید دقت داشت که این مقدار باید درون '' قرار گیرد تا مشخص شود که رشته‌ای به خاصیت position انتساب داده شده‌است و این مقدار یک خاصیت عمومی تعریف شده‌ی در کامپوننت متناظر با قالب، نیست.


نمایش یک پیام خطا توسط ToastyService

اکنون که کار برپایی کامپوننت Angular2 Toasty به پایان رسید، کار کردن با آن به سادگی تزریق سرویس آن به سازنده‌ی یک کامپوننت و فراخوانی متدهای info، success ، wait ، error و warning آن است:
import { ToastyService, ToastOptions } from "ng2-toasty";

export class ProductGroupComponent implements OnInit {

  constructor(
    private productItemsService: ProductItemsService,
    private toastyService: ToastyService) { }

  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: "Please select a category.",
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
      return;
    }
- در اینجا در ابتدا ماژول‌های مورد نیاز import شده‌اند.
- سپس ToastyService به سازنده‌ی کلاس کامپوننت مدنظر تزریق شده‌است تا بتوان از امکانات آن استفاده کرد.
- در ادامه، فراخوانی متد this.toastyService.error سبب نمایش اخطار قرمز رنگی می‌شود که تصویر آن‌را در ابتدای مطلب جاری مشاهده کردید.
- علت ذکر <ToastOptions> در اینجا این است که وجود آن سبب خواهد شد تا intellisense در VSCode فعال شود و پس از آن بتوان تمام گزینه‌های این متد و تنظیمات را بدون مراجعه‌ی به مستندات آن از طریق intellisense یافت و درج کرد:



مدیریت سراسری خطاهای مدیریت نشده، در یک برنامه‌ی Angular

در برنامه‌های Angular از این دست کدها بسیار مشاهده می‌شوند:
    this.productItemsService.getCategories().subscribe(
      data => {
        this.categories = data;
      },
      err => console.log("get error: ", err)
    );
تا اینجا قسمت err یا بروز خطا را با console.log مدیریت کرده‌ایم. در این حالت کاربر ممکن است 10 بار بر روی دکمه‌ای کلیک کند یا صفحه‌ای را بارگذاری کند و دست آخر متوجه نشود که مشکل کار چیست. به همین جهت می‌توان خطاها را نیز توسط ToastyService نمایش داد تا کاربران دقیقا متوجه بروز مشکل رخ داده شوند. اما ... به این ترتیب تکرار کد زیادی را خواهیم داشت و باید به ازای تمام این موارد، یکبار this.toastyService.error را فراخوانی کنیم. برای مدیریت بهتر یک چنین سناریویی در Angular، کلاس و سرویس توکاری به نام ErrorHandler وجود دارد. در هر قسمتی از برنامه‌ی Angular که استثنایی مدیریت نشده رخ دهد، ابتدا از این کلاس رد شده و سپس به برنامه انتشار پیدا می‌کند. بنابراین می‌توان یک ErrorHandler سفارشی را با ارث بری از آن تهیه کرد و سپس بجای سرویس توکار اصلی، به برنامه معرفی و از آن استفاده نمود. به این ترتیب می‌توان یک Global Error Interceptor را طراحی نمود.
به همین منظور کلاس جدیدی را به صورت ذیل در پوشه‌ی src\app اضافه می‌کنیم:
> ng g cl app.error-handler
با این خروجی
 installing class
  create src\app\app.error-handler.ts
سپس این کلاس را به نحو ذیل تکمیل خواهیم کرد:
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  handleError(error: any): void {
    console.log("Error:", error);
  }
}
کلاس جدید AppErrorHandler از کلاس پایه ErrorHandler ارث بری می‌کند. بنابراین import آن‌را در ابتدای کار مشاهده می‌کنید. سپس باید متد handleError آن‌را با امضایی که مشاهده می‌کنید، پیاده سازی کنیم. فعلا با استفاده از console.log این خطا را در کنسول developer tools نمایش می‌دهیم.

اکنون نیاز است این ErrorHandler سفارشی را بجای نمونه‌ی اصلی به برنامه معرفی کنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و تغییرات ذیل را اعمال می‌کنیم:
import { NgModule, ErrorHandler } from "@angular/core";
import { AppErrorHandler } from "./app.error-handler";

@NgModule({
  providers: [
    { provide: ErrorHandler, useClass: AppErrorHandler }
  ]
ابتدا ErrorHandler به لیست imports اضافه شده‌است و همچنین محل تامین AppErrorHandler نیز مشخص گردیده‌است. سپس در قسمت providers ماژول جاری، از تعریف خاصی که ملاحظه می‌کنید، استفاده خواهد شد. به این ترتیب به Angular اعلام می‌کنیم، هرگاه نیازی به وهله‌ای از کلاس توکار ErrorHandler بود، وهله‌ای از کلاس سفارشی AppErrorHandler را مورد استفاده قرار بده.

اکنون برای آزمایش آن، در کدهای سمت سرور مطلب «ایجاد Drop Down List‌های آبشاری در Angular»، یک استثنای عمدی را قرار می‌دهیم:
[HttpGet("[action]/{categoryId:int}")]
public async Task<IActionResult> GetProducts(int categoryId)
{
   throw new Exception();
به این ترتیب هر زمانیکه گروهی انتخاب شد، دریافت محصولات آن گروه با خطا مواجه می‌شود.
برای اینکه AppErrorHandler، مورد استفاده قرار گیرد، قسمت err دریافت لیست محصولات را نیز حذف می‌کنیم (تا تبدیل به یک استثنای مدیریت نشده شود):
    this.productItemsService.getProducts(categoryId).subscribe(
      data => {
        this.products = data;
        this.isLoadingProducts = false;
      }// ,
      // err => {
      //   console.log("get error: ", err);
      //   this.isLoadingProducts = false;
      // }
    );
اکنون اگر برنامه را اجرا کنیم، چنین پیامی، در کنسول developer tools ظاهر می‌شود و مشخص است از فایل AppErrorHandler صادر شده‌است:



افزودن ToastyService به AppErrorHandler

در ادامه می‌خواهیم بجای console.log از ToastyService برای نمایش خطاهای مدیریت نشده‌ی برنامه در کلاس AppErrorHandler استفاده کنیم:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(private toastyService: ToastyService) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);
    this.toastyService.error(<ToastOptions>{
      title: "Error!",
      msg: "Fatal error!",
      theme: "bootstrap",
      showClose: true,
      timeout: 5000
    });
  }
}
به همین منظور سرویس آن‌را به سازنده‌ی کلاس AppErrorHandler تزریق کرده و سپس از آن به نحو متداولی در متد handleError استفاده می‌کنیم. به این ترتیب بجای ده‌ها و یا صدها قسمت مدیریت err=>this.toastyService.error در برنامه، تنها یک مورد مدیریت مرکزی را خواهیم داشت.

مشکل اول! اکنون اگر برنامه را اجرا کنیم، در کنسول developer tools چنین خطایی ظاهر می‌شود:
 Uncaught Error: Can't resolve all parameters for AppErrorHandler: (?).
به این معنا که Angular قادر نیست وهله‌ای از AppErrorHandler را ایجاد کند؛ چون نمی‌داند که چگونه باید پارامتر سازنده‌ی ToastyService را وهله سازی و تزریق نماید. علت اینجا است که کار آغاز کلاس ویژه‌ی ErrorHandler سراسری، پیش از کار بارگذاری ماژول مرتبط با ToastyService انجام می‌شود. به همین جهت، این مورد جزو معدود مواردی است که باید به صورت دستی تزریق شود:
import { ErrorHandler, Inject } from "@angular/core";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(ToastyService) private toastyService: ToastyService
  ) {
  }
در اینجا توسط Inject decorator، کار تزریق دستی ToastyService انجام خواهد شد. اکنون اگر برنامه را مجدد اجرا کنیم، خطای قبلی برطرف شده‌؛ یعنی کلاس AppErrorHandler با موفقیت وهله سازی شده‌است.

مشکل دوم! اینبار برنامه را اجرا کنید. سپس گروهی را انتخاب نمائید. مشاهده می‌کنید که خطایی نمایش داده نشد؛ هرچند در کنسول developer tools می‌توان اثری از آن را مشاهده کرد. مجددا گروه دیگری را انتخاب کنید، در این بار دوم است که خطای ارائه شده‌ی توسط this.toastyService.error ظاهر می‌شود. توضیح آن نیاز به بررسی مفهومی به نام Zones در Angular دارد.


مفهوم Zones در Angular

زمانیکه متد this.toastyService.error در یک کامپوننت برنامه مورد استفاده قرار گرفت، به خوبی کار می‌کرد و در همان بار اول فراخوانی، پیام را نمایش می‌داد. اما با انتقال آن به کلاسAppErrorHandler ، این قابلیت از کار افتاد. علت اینجا است که زمینه‌ی اجرایی این قطعه کد، اکنون خارج از Zone یا ناحیه‌ی Angular است و به همین دلیل متوجه تغییرات آن نمی‌شود. Zone زمینه‌ی اجرایی اعمال async است و اگر به فایل package.json یک برنامه‌ی Angular دقت کنید، بسته‌ی zone.js، یکی از وابستگی‌های همراه آن است.
تغییرات حالت برنامه، توسط یکی از اعمال ذیل رخ می‌دهند:
الف) بروز رخ‌دادهایی مانند کلیک، ورود اطلاعات و یا ارسال فرم
ب) اعمال Ajax ایی
ج) استفاده از Timers مانند استفاده از setTimeout و  setInterval

هر سه مورد یاد شده از نوع async بوده و زمانیکه رخ می‌دهند، حالت برنامه را تغییر خواهند داد. Angular نیز تنها به این موارد علاقمند بوده و به آ‌ن‌ها در جهت به روز رسانی رابط کاربری برنامه واکنش نشان می‌دهد.
برای مثال this.toastyService.error دارای خاصیتی است به نام timeout: 5000 که در آن، مورد «ج» فوق رخ می‌دهد؛ یعنی یک Timer پس از 5 ثانیه سبب بسته شدن آن خواهد شد. به همین جهت است که اگر پیش از پایان این 5 ثانیه مجددا درخواست واکشی لیست محصولات یک گروه را بدهیم، خطای مربوطه مشاهده می‌شود. چون Angular زمینه‌ی اجرایی لازم را فراهم کرده (یا همان Zone در اینجا) و مجبور به واکنش به عملیات async از نوع Timer است.

برای دسترسی به امکانات کتابخانه‌ی zone.js، می‌توان از طریق تزریق سرویس آن به نام NgZone به سازنده‌ی کلاس شروع کرد:
import { ToastyService, ToastOptions } from "ng2-toasty";
import { ErrorHandler, Inject, NgZone } from "@angular/core";
import { LocationStrategy, PathLocationStrategy } from "@angular/common";

export class AppErrorHandler implements ErrorHandler {

  constructor(
    @Inject(NgZone) private ngZone: NgZone,
    @Inject(ToastyService) private toastyService: ToastyService,
    @Inject(LocationStrategy) private locationProvider: LocationStrategy
  ) {
  }

  handleError(error: any): void {
    // console.log("Error:", error);

    const url = this.locationProvider instanceof PathLocationStrategy ? this.locationProvider.path() : "";
    const message = error.message ? error.message : error.toString();
    this.ngZone.run(() => {
      this.toastyService.error(<ToastOptions>{
        title: "Error!",
        msg: `URL:${url} \n ERROR:${message}`,
        theme: "bootstrap",
        showClose: true,
        timeout: 5000
      });
    });

    // IMPORTANT: Rethrow the error otherwise it gets swallowed
    // throw error;
  }
}
در اینجا فراخوانی this.ngZone.run سبب می‌شود تا درخواست نمایش خطای رخ‌داده وارد Angular Zone شده و بلافاصله سبب نمایش آن گردد:
 


چند نکته
1- اگر می‌خواهید علاوه بر رخ‌دادگردانی سراسری خطاها، این خطاها را به محل اصلی آن‌ها نیز انتشار دهید، نیاز است سطر throw error را در انتهای متد handleError نیز ذکر کنید. در غیر اینصورت، کار در همینجا به پایان خواهد رسید و این خطاها دیگر منتشر نمی‌شوند.
2- روش دریافت URL جاری صفحه را نیز در اینجا مشاهده می‌کنید. این اطلاعات می‌توانند جهت ارسال به سرور برای ثبت و بررسی‌های بعدی مفید باشند.
3- مقدار new Error().stack معادل stack trace جاری است و تقریبا در تمام مرورگرهای جدید پشتیبانی می‌شود.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-07.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
تهیه پردازنده‌های سفارشی برای افزونه XMLWorker کتابخانه iTextSharp
پیشتر مطلب «تهیه پردازنده‌های سفارشی برای HTMLWorker کتابخانه iTextSharp» را در این سایت مطالعه کرده‌اید. از آنجائیکه افزونه HTMLWorker منسوخ شده است و دیگر پشتیبانی نخواهد شد، باید کدهای فعلی را به افزونه XMLWorker منتقل کرد. مقدمه‌ای را در این زمینه در مطلب «تبدیل HTML فارسی به PDF با استفاده از افزونه‌ی XMLWorker کتابخانه‌ی iTextSharp» می‌توانید مطالعه نمائید.
در ادامه قصد داریم همان امکان پشتیبانی از تصاویر base64 مدفون شده در صفحات HTML را به کتابخانه XMLWorker نیز اضافه کنیم.


تهیه پردازنده‌های سفارشی تگ‌های HTML جهت افزونه XMLWorker

در اینجا کار با ارث بری از کلاس AbstractTagProcessor شروع می‌شود. سپس کافی است تا متد End این کلاس پایه را override کرده و تگ سفارشی خود را پردازش کنیم. نمونه‌هایی از این نوع پردازنده‌ها را در پوشه itextsharp.xmlworker\iTextSharp\tool\xml\html سورس‌های iTextSharp می‌توانید ملاحظه کنید و جهت ایده گرفتن بسیار مناسب می‌باشند.
یکی از این پردازنده‌های پیش فرض موجود در افزونه XMLWorker کار پردازش تگ‌های img را انجام می‌دهد و در کلاس iTextSharp.tool.xml.html.Image قرار گرفته است. این پردازنده به صورت پیش فرض تصاویر base64 را پردازش نمی‌کند. برای رفع این مشکل می‌توان به نحو ذیل عمل کرد:
    public class CustomImageTagProcessor : iTextSharp.tool.xml.html.Image
    {
        public override IList<IElement> End(IWorkerContext ctx, Tag tag, IList<IElement> currentContent)
        {
            IDictionary<string, string> attributes = tag.Attributes;
            string src;
            if (!attributes.TryGetValue(HTML.Attribute.SRC, out src))
                return new List<IElement>(1);

            if (string.IsNullOrEmpty(src))
                return new List<IElement>(1);

            if (src.StartsWith("data:image/", StringComparison.InvariantCultureIgnoreCase))
            {
                // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
                var base64Data = src.Substring(src.IndexOf(",") + 1);
                var imagedata = Convert.FromBase64String(base64Data);
                var image = iTextSharp.text.Image.GetInstance(imagedata);

                var list = new List<IElement>();
                var htmlPipelineContext = GetHtmlPipelineContext(ctx);
                list.Add(GetCssAppliers().Apply(new Chunk((iTextSharp.text.Image)GetCssAppliers().Apply(image, tag, htmlPipelineContext), 0, 0, true), tag, htmlPipelineContext));
                return list;
            }
            else
            {
                return base.End(ctx, tag, currentContent);
            }
        }
    }
با ارث بری از کلاس پردازنده پیش فرض تگ‌های تصاویر یا iTextSharp.tool.xml.html.Image شروع و سپس متد End آن‌را  تحریف کرده‌ایم.
در اینجا اگر src یک تگ img با الگوی تصاویر base64 شروع شده باشد، تصویر آن استخراج و تبدیل به وهله‌ای از تصاویر استاندارد iTextSharp می‌شود. سپس این تصویر در اختیار htmlPipelineContext قرار داده خواهد شد و یا اگر این تصویر از نوع base64 نباشد، همان متد base.End کلاس پایه، جهت پردازش آن کفایت می‌کند.


استفاده از یک پردازنده تگ سفارشی HTML افزونه XMLWorker

تا اینجا یک پردازنده جدید تصاویر را ایجاد کرده‌ایم. برای اینکه XMLWorker را از وجود آن مطلع کنیم، نیاز است آن‌را به درون htmlPipeline این افزونه تزریق نمائیم که کدهای کامل آن‌را در ذیل مشاهده می‌کنید:
            using (var doc = new Document(PageSize.A4))
            {
                var writer = PdfWriter.GetInstance(doc, new FileStream("test.pdf", FileMode.Create));
                doc.Open();
                var html = @"<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABQCAMAAAB24TZcAAAABGdBTUEAANbY1E9YMgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAGAUExURdSmeJp2SHlbQIRoSUg2J499a8KebqeHZuGufBEVJPz7+3NWPVxGMduwhPXEktnX1mtROLq7t5WDc2VMNv3LmKB8TMSidMbFxLGlmXlhSMSddpJUL+y8i3VlVqedlOzr6gUIF2lXRLCLY4ZyXLyYaYhtUYiJhJFyU1dBLLiVZnlwZrWRY/Hx8b+2rbySaJh9YqeooDw4NygnKvvJlpyblzksIUhGRryYckc7MPjGlKODX5x8VVA8K+azgM3FvDInHK2JW2ZbUOHh4Xt2cFpaWKeAUM6kel1RRJmUjo5vSrWzrJJ1WFhLQCQmMuK1iJiMgmthWPPCkOm3hEtBOunm5LCNXnJtZquEXmNkYvG+i7Ctq+y5hrWRbKqSeaN/WqmFVYFgQh8aGOa4isWkd8mcby4vONDNy0AwI5h2U19JMxkdLzIuL1JBMjQ3P5Z6Ve6/j93c2+Xi34KAfJ5/Xvj4+O/u7sSKVJd4Wo6QjXE+IeOwfQcNJoBeQ8Gdbf/Mmf///5GX6NEAAAcrSURBVHja3JbpX9pIGMchiWkgEaOBtaGinBLEyopFBeMqtYKI4kGt2lILFsUoXa3WdZcc/dd3JheHAvaz7/Z5Ec2Q7/yeaw7Lz/9klv8rfnM+Orz5cXLjZsL+67h9eCq9Vaxvzc6v3W6+/TX85kN6ixdokkQQCaE5vrg28Qv4a2yFQcpSi/HzH6efi+/UaEAwWAtepuvv3tw/B//hqZGQqDFSmyHC7v0z8EldlZQQEgTfMgF23h8/T+gEhQGrcQYrMBKVtvfDb4qU/j3DMK3SdIKWsNs++M1iS8R8W/gULyG1771w+/stQWpTpFpzByb09MRHEwaoxUxToGtaZiBrE72cXzMyhcDiIRgCHxJPIxKt5aF23gMf0iquz8BJmAAFpUStxvG0xIA3arcHPsvrJM1wvFTDeEGQeKCewCo1jgRDwKuJrrh9C3osIfyiz+NboZFKxU0xJEYmeJbBhPoKiKyMDXfHd0mJWSETnoKiKCmgSioFDKFr4T1lbn/fgkHf+PGu+A+A12imMqdAqzNUXlFCFP+gOD41CKJBcCB4bKSnOmitB5VWSgnMrSjhCnu8D1hoS1xP/KcH1BhZdGi4c4VNAh/I5PGyRjdQqje+A6YXPIpup/DhHlMUh44f1hAJ6x77z3OwVjG/0ml7Ot4gOWnxvkfbALw+2EnPGc43ojWk3qNt7hdpiSp0ajcMukHQPB/4o3vPf8TKQgc+pqXdkpEtgGewE7THel/j66dtdBLA1XAYRXK8AGbxC/6RHvjbCuOE0Kklk8lcg/+OicaJcOhfTflTVYCHuYvX3XH7QCxcUAol9i6VursLha+VfcLPHwamZjfSAgxi6QId6oFnC5awsjdoWYjFPrOlB3QONAtJjrwsetiq2jkzgfc9nPdklJBDyXvGj+Zf+jIKe7pPoNFoOHwyoyaQKFcD9z3wzbwSGnT6fCMB9u5UmWMLYwTJQo5QC2AB6r122ukBJeVWnA6HIwlLnp/bI/w5wI3tJR3LjcZMbvVzL/xHwOG+M6s2mFeSjRm0QRyDYnyCOEv/0fOYGM/vha4N3J1S5hoZhCAcYBro/AwV63NIjafuzL4rLSjOZYKeIT45j9XUnQTs/Y7Inbqp/pABeIPBqsTystr0/pd9T9jprZIGO9CHa4gTPHairxr/eP/rwai+YdzlWQfALSHu4qTxfHxiQKVTaBINvfCjDFo1Fmzjor/zP+0BNXdgxSTdqRe5w0bT2hq+293mdWDOSJ5DWbgwd4uGpSPxXW5WGzGddhYWHsDRguqpO5x9jjq4HY3BnjtcRRGGe/Xqn38YC6SraVt84jnXwo0FgC8kOK7s+mv91St6RhVnZ72Vqeln4EM+cFY43SHgdj584c9ormdFbx3Jbk73v9PuvNCCvx67ntPzlmG2xUvUhQpZz9roxHdwXx4e7Yb/fdXc7o81PFcUxW2ry+Wy5miM4gQkEAh0uxKfXWbdLXs1XGxZURRnXZpZrVbXegT/rUvm571itnncQPctWZso2hAdd61GIzIuf32y5zduL0VxtwQPWG2vB7QP0OKKVaejOI7L8lP4+S3r+wY+zSZfGPvGPlFlt8FQ3BCPQPYpfOjWs3QHtMVLJqmU0NLe9XVhsBpOwyER0+D1oE534t8Hsn/KctwLokxUgeunD6FwCA2xMGtAPAdhjkr55afwoaksGpHlAKTnWUK9ZIAt15k/U+mK5voSuoI9Vre/fZPOBcFQKg4+PXsXg7urVra0Stvqmud4mTp4hN/s+lAIy8ErIC7Oz8aITzqegYkUL4tawQ+ivEvudP7Gt6SPpCpewJ8BfN+pb/aq71dG2kjayLuJ3/vC+gB+EBe9Xm/8KEQs67hShMmgIRsNylFuFe9UL1IGHXHNAtr77ZYN7htNB8LxJmCnyaBZULpJ6/g4ZZQCX83FAS1u3675xnTaX/GKFdLl+gIaDZeFpU78rS9oDnzZEmHstqPJKc9n90LJPThyBUZIVRtMv8Q1v9Xx8bzxigddWo1t7yZ//zgSCwRiK6CO0PUD2OR4hMnhHfiPtYiJr4a8Jj4MbHNe7UC4RtTfc5wsd+DD6RbxxTZ8chtkrcJGIlqX41GqTVzFp3wmfmCNi5rNT74Z3nwHi2BjZW11AtdzgvxIfSBl4l/Klzr+bfLvzSNYA1u9xTfmz8f4lLmA5HWfgV8eTa7BEohxox1xeZ1F5Ef4fTrYnL4oGjb7QZ3JVgk2W4KJPMZvmWbo9KWJ27QsXKHm3DkhJT/Gs6z55lo0abV5wCSL5txL/CMa4PYPUXN+5qwTj68aXwa5MP4Efj/VDA4TW3BV3PQMp7Wlgnfg555mcPFO8RbXMbXv8Oh6pG3J7IRM8bq3Q/zKLFqUQ3GteNYvbepG1XG57O0Qt9Hmd1bOKC1qbZH/zbK78FWzYMJ2aZoXPq7kr8ZvORr+iUSjJzQb/Gpa5l8BBgBZTppAyfsf0wAAAABJRU5ErkJggg==' width='62' height='80' style='float: left; margin-right: 28px;' />";

                var tagProcessors = (DefaultTagProcessorFactory)Tags.GetHtmlTagProcessorFactory();
                tagProcessors.RemoveProcessor(HTML.Tag.IMG); // remove the default processor
                tagProcessors.AddProcessor(HTML.Tag.IMG, new CustomImageTagProcessor()); // use our new processor

                var cssResolver = new StyleAttrCSSResolver();
                cssResolver.AddCss(@"code { padding: 2px 4px; }", "utf-8", true);
                var charset = Encoding.UTF8;
                var hpc = new HtmlPipelineContext(new CssAppliersImpl(new XMLWorkerFontProvider()));
                hpc.SetAcceptUnknown(true).AutoBookmark(true).SetTagFactory(tagProcessors); // inject the tagProcessors
                var htmlPipeline = new HtmlPipeline(hpc, new PdfWriterPipeline(doc, writer));
                var pipeline = new CssResolverPipeline(cssResolver, htmlPipeline);
                var worker = new XMLWorker(pipeline, true);
                var xmlParser = new XMLParser(true, worker, charset);
                xmlParser.Parse(new StringReader(html));
            }
            Process.Start("test.pdf");
در اینجا ابتدا لیست پردازنده‌های پیش فرض افزونه XMLWorker را دریافت و سپس پردازنده تگ img آن‌را حذف و با نمونه جدید خود جایگزین کرده‌ایم. در ادامه این لیست تغییر یافته به درون HtmlPipelineContext تزریق شده‌است تا بجای DefaultTagProcessorFactory اصلی مورد استفاده قرار گیرد.