نظرات مطالب
شروع به کار با EF Core 1.0 - قسمت 10 - استفاده از امکانات بومی بانک‌های اطلاعاتی
رفع محدودیت «خروجی کوئری SQL، تنها باید معادل یکی از کلاس‌های موجودیت‌های شما باشد» در نگارش 2.1
در نگارش 2.1 مفهوم جدیدی به نام Query Types ارائه شده‌است که امکان نگاشت به خروجی‌های خاص بانک اطلاعاتی مانند Viewها و یا رویه‌های ذخیره شده را میسر می‌کند که این خروجی‌ها عموما مستقل از فیلدهای جداول و موجودیت‌های تعریف شده‌ی در برنامه هستند.
برای مثال فرض کنید یک View ویژه را بر اساس جدول و یا جداول بانک اطلاعاتی خود طراحی کرده‌اید:
using (var db = new BloggingContext())
{
   db.Database.ExecuteSqlCommand(
         @"CREATE VIEW View_BlogPostCounts AS 
             SELECT Name, Count(p.PostId) as PostCount from Blogs b
             JOIN Posts p on p.BlogId = b.BlogId
             GROUP BY b.Name");
}
خروجی این View که دو ستون name و PostCount را به همراه دارد، متناظر با موجودیت‌های اصلی برنامه نیست. برای تهیه نگاشتی به آن، ابتدا کلاس مدل متناظر با این ستون‌های بازگشتی را تهیه می‌کنیم:
public class BlogPostsCount
{
    public string BlogName { get; set; }
    public int PostCount { get; set; }
}

سپس برای معرفی آن به Context باید دو مرحله انجام شود:
الف) این کلاس به صورت DbQuery در Context معرفی می‌شود:
public class BloggingContext : DbContext
{
    public DbQuery<BlogPostsCount> BlogPostCounts { get; set; }
ب) در متد OnModelCreating همین Context، نگاشت این DbQuery به View یاد شده توسط متد ToView انجام می‌شود:
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
      modelBuilder
           .Query<BlogPostsCount>().ToView("View_BlogPostCounts")
           .Property(v => v.BlogName).HasColumnName("Name");
    }
متد ToView الزاما نیازی به یک view ندارد. مفهوم آن صرفا یک خروجی فقط خواندنی است. برای مثال حتی در اینجا یک جدول بانک اطلاعاتی را هم می‌توانید ذکر کنید. اما مفهوم آن غیرقابل تغییر بودن خروجی کوئری‌های آن است. بنابراین باید دقت داشت که در اینجا مهم نیست که کلاس نگاشت تعریف شده دارای کلید هست یا خیر و ارجاعی از این کلاس را نمی‌توان در کلاس‌های موجودیت‌های اصلی مورد استفاده قرار داد.
از متد Property به این جهت استفاده شده‌است که در کلاس BlogPostsCount، خاصیت BlogName، متناظر با هیچکدام از ستون‌های بازگشتی View تعریف شده نیست. به همین جهت با استفاده از این متد مشخص کرده‌ایم که این خاصیت باید به کدام ستون بازگشتی، نگاشت شود.
و در آخر کوئری گرفتن از این DbQuery تعریف شده به صورت زیر است:
using (var db = new BloggingContext())
{
   var postCounts = db.BlogPostCounts.ToList();

مثال کامل این نکته
مطالب
تبدیل html به pdf با کیفیت بالا

کتابخانه iTextSharp دارای کلاسی است به نام HTMLWorker که کار تبدیل عناصر HTML را به عناصر متناظر خودش، انجام می‌دهد. این کلاس در حال حاضر منسوخ شده درنظر گرفته می‌شود (اینطور توسط نویسندگان آن اعلام شده) و دیگر توسعه نخواهد یافت. بنابراین اگر از HTMLWorker استفاده می‌کنید با یک کلاس قدیمی که دارای HTML Parser ایی بسیار بدوی است طرف هستید و در کل برای تبدیل محتوای HTML ایی با ساختار بسیار ساده بد نیست؛ اما انتظار زیادی از آن نداشته باشید.
جایگزین کلاس HTMLWorker در این کتابخانه در حال حاضر کتابخانه itextsharp.xmlworker است، که به صورت یک افزونه در کنار کتابخانه اصلی در حال توسعه می‌باشد. مشکل اصلی این کتابخانه، عدم پشتیبانی از UTF8 و راست به چپ است. بنابراین حداقل به درد کار ما نمی‌خورد.

راه حل بسیار بهتری برای موضوع اصلی بحث ما وجود دارد و آن هم استفاده از موتور WebKit (همان موتوری که برای مثال در Apple Safari استفاده می‌شود) برای HTML parsing و سپس تبدیل نتیجه نهایی به PDF است. پروژه‌ای که این مقصود را میسر کرده، wkhtmltopdf نام دارد.
توسط آن به کمک موتور WebKit، کار HTML Parsing انجام شده و سپس برای تبدیل عناصر نهایی به PDF از امکانات کتابخانه‌ای به نام QT استفاده می‌شود. کیفیت نهایی آن کپی مطابق اصل HTML قابل مشاهده در یک مرورگر است و با یونیکد و زبان فارسی هم مشکلی ندارد.

برای استفاده از این کتابخانه‌ی native در دات نت، شخصی پروژه‌ای را ایجاد کرده است به نام WkHtmlToXSharp که محصور کننده‌ی wkhtmltopdf می‌باشد. در ادامه به نحوه استفاده از آن خواهیم پرداخت:

الف) دریافت پروژه WkHtmlToXSharp
پروژه WkHtmlToXSharp را از آدرس زیر می‌توانید دریافت کنید.

 این پروژه به همراه فایل‌های کامپایل شده نهایی wkhtmltopdf نیز می‌باشد و حجمی حدود 40 مگ دارد. به علاوه فعلا نسخه 32 بیتی آن در دسترس است. بنابراین باید دقت داشت که نباید تنظیمات پروژه دات نت خود را بر روی Any CPU قرار دهیم، زیرا در این حالت برنامه شما در یک سیستم 64 بیتی بلافاصله کرش خواهد کرد. تنظیمات target platform پروژه دات نتی ما حتما باید بر روی X86 تنظیم شود.

ب) پس از دریافت این پروژه و افزودن ارجاعی به اسمبلی WkHtmlToXSharp.dll، استفاده از آن به نحو زیر می‌باشد:

using System.IO;
using WkHtmlToXSharp;
using System;

namespace Test2
{
    public class WkHtmlToXSharpTest
    {
        public static void ConvertHtmlStringToPdfTest()
        {
            using (var wk = new MultiplexingConverter())
            {
                wk.Begin += (s, e) => Console.WriteLine("Conversion begin, phase count: {0}", e.Value);
                wk.Error += (s, e) => Console.WriteLine(e.Value);
                wk.Warning += (s, e) => Console.WriteLine(e.Value);
                wk.PhaseChanged += (s, e) => Console.WriteLine("PhaseChanged: {0} - {1}", e.Value, e.Value2);
                wk.ProgressChanged += (s, e) => Console.WriteLine("ProgressChanged: {0} - {1}", e.Value, e.Value2);
                wk.Finished += (s, e) => Console.WriteLine("Finished: {0}", e.Value ? "success" : "failed!");

                wk.GlobalSettings.Margin.Top = "0cm";
                wk.GlobalSettings.Margin.Bottom = "0cm";
                wk.GlobalSettings.Margin.Left = "0cm";
                wk.GlobalSettings.Margin.Right = "0cm";

                wk.ObjectSettings.Web.EnablePlugins = false;
                wk.ObjectSettings.Web.EnableJavascript = false;
                wk.ObjectSettings.Load.Proxy = "none";

                var htmlString = File.ReadAllText(@"c:\page.xhtml");
                var tmp = wk.Convert(htmlString);

                File.WriteAllBytes(@"tst.pdf", tmp);
            }
        }
    }
}

کار با وهله سازی از کلاس MultiplexingConverter شروع می‌شود. اگر علاقمند باشید که درصد پیشرفت کار به همراه خطاهای احتمالی پردازشی را ملاحظه کنید می‌توان از رخدادگردان‌هایی مانند ProgressChanged و Error استفاده نمائید که نمونه‌ای از آن در کد فوق بکارگرفته شده است.
تبدیل HTML به PDF آنچنان تنظیمات خاصی ندارد زیرا فرض بر این است که قرار است از همان تنظیمات اصلی HTML مورد نظر استفاده گردد. اما اگر نیاز به تنظیمات بیشتری وجود داشت، برای مثال به کمک GlobalSettings آن می‌توان حاشیه‌های صفحات فایل نهایی تولیدی را تنظیم کرد.
موتور WebKit با توجه به اینکه موتور یک مرورگر است، امکان پردازش جاوا اسکریپت را هم دارد. بنابراین اگر قصد استفاده از آن‌را نداشتید می‌توان خاصیت ObjectSettings.Web.EnableJavascript را به false مقدار دهی کرد.
کار اصلی، در متد Convert انجام می‌شود. در اینجا می‌توان یک رشته را که حاوی فایل HTML مورد نظر است به آن ارسال کرد و نتیجه نهایی، آرایه‌ای از بایت‌ها، حاوی فایل باینری PDF تولیدی است.
روش دیگر استفاده از این کتابخانه، مقدار دهی wk.ObjectSettings.Page می‌باشد. در اینجا می‌توان Url یک صفحه اینترنتی را مشخص ساخت. در این حالت دیگر نیازی نیست تا به متد Convert پارامتری را ارسال کرد. می‌توان از overload بدون پارامتر آن استفاده نمود.

یک نکته:
اگر می‌خواهید زبان فارسی را توسط این کتابخانه به درستی پردازش کنید، نیاز است حتما یک سطر زیر را به header فایل html خود اضافه نمائید:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

 
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 3 - Middleware چیست؟
پیشنیازها
- «با HttpHandler بیشتر آشنا شوید»
یکی از بزرگترین تغییرات ASP.NET Core نسبت به نگارش‌های قبلی آن، مدیریت HTTP pipeline آن است. به عنوان یک توسعه دهنده‌ی ASP.NET به طور قطع با مفاهیمی مانند HttpHandler و HttpModules آشنایی دارید و ... هر دوی این‌ها با نگارش جدید ASP.NET حذف و با مفهوم جدیدی به نام Middleware جایگزین شده‌اند.
 - HttpHandlerها عموما مبتنی بر پسوندهای فایل‌ها عمل می‌کنند. برای نمونه، رایج‌ترین آن‌ها ASP.NET page handler است که هرگاه درخواستی، ختم شده‌ی به پسوند aspx، به موتور ASP.NET Web forms وارد شد، به این page handler، جهت پردازش نهایی هدایت می‌شود. محل تنظیم آن‌ها نیز در فایل web.config است.
 - HttpModuleها مبتنی بر رخدادها عمل می‌کنند. HttpModuleها به عنوان جزئی از request pipeline عمل کرده و دسترسی کاملی دارند به رخدادهای طول عمر درخواست رسیده. آن‌ها را می‌توان توسط فایل‌های global.asax و یا web.config تنظیم کرد.
 - MiddleWareها را که جزئی از طراحی OWIN نیز هستند، می‌توان به عنوان کامپوننت‌های کوچکی از برنامه که قابلیت یکپارچه شدن با HTTP request pipeline را دارند، درنظر گرفت. عملکرد آن‌ها ترکیبی است از هر دوی HttpHandler و HttpModule‌ها که در نگارش‌های قبلی ASP.NET مورد استفاده بودند. از MiddleWareها می‌توان برای پیاده سازی اعمال مختلفی جهت پردازش درخواست‌های رسیده مانند اعتبارسنجی، کار با سشن‌ها، ثبت وقایع سیستم، مسیریابی و غیره استفاده کرد. OWIN یا Open Web Interface for .NET به توسعه دهنده‌ها امکان غیروابسته کردن برنامه‌ی ASP.NET خود را از وب سرور مورد استفاده می‌دهد. به علاوه OWIN امکان پلاگین نویسی اجزای مختلف برنامه را بدون وابسته کردن آن‌ها به یکدیگر فراهم می‌کند. برای مثال می‌توان یک پلاگین logger را تهیه کرد تا اطلاعات مختلفی را از درخواست‌های رسیده ثبت کند.



مواردی که با ارائه‌ی ASP.NET Core 1.0 حذف شده‌اند

- System.Web: یکی از اهداف اصلی OWIN، مستقل کردن برنامه‌ی وب، از هاست آن است تا بتوان از وب سرورهای بیشتری استفاده کرد. System.Web انحصارا برای IIS طراحی شده بود و در این نگارش دیگر وجود خارجی ندارد.
- HttpModules: با Middlewareها جایگزین شده‌اند.
- HttpHandlers: البته HttpHandlers از زمان ارائه‌ی اولین نگارش ASP.NET MVC، در چندین سال قبل منسوخ شدند. زیرا پردازش صفحات وب در ASP.NET MVC برخلاف وب فرم‌ها، از فایل‌هایی با پسوندهای خاص شروع نمی‌شوند و نقطه‌ی آغازین آن‌ها، اکشن متدهای کنترلرها است.
- فایل global.asax: با حذف شدن HttpModules، دیگر ضرورتی به وجود این فایل نیست.


شباهت‌های بینMiddleware و HttpModuleها

همانند HttpModuleها، Middlewareها نیز به ازای هر درخواست رسیده اجرا می‌شوند و از هر دو می‌توان جهت تولید response ایی خاص استفاده کرد.


تفاوت‌های بینMiddleware و HttpModuleها

- عموما HttpModule از طریق web.config و یا فایل global.asax تنظیم می‌شوند. اما Middlewareها تنها از طریق کد و در فایل Startup.cs برنامه‌های ASP.NET Core 1.0 قابل معرفی هستند.
- به عنوان یک توسعه دهنده، کنترلی را بر روی ترتیب اجرای انواع و اقسام HttpModuleها نداریم. این مشکل در Middlewareها برطرف شده و اکنون ترتیب اجرای آن‌ها، دقیقا مطابق ترتیب افزوده شدن و تعریف آن‌ها در فایل Startup.cs است.
- ترتیب اجرای HttpModuleها هرچه که باشد، برای حالت‌های request و response یکی است. اما ترتیب اجرای Middlewareهای مخصوص response، عکس ترتیب اجرای Middlewareهای مخصوص request هستند (تصویر ذیل).
- HttpModuleها را صرفا جهت اتصال کدهایی به رخدادهای طول عمر برنامه می‌توان طراحی کرد؛ اما Middleware مستقل هستند از این رخدادها.
- HttpModuleها به System.Web وابسته‌اند؛ برخلاف Middlewareها که وابستگی به هاست خود ندارند.



Middleware‌های توکار ASP.NET Core 1.0

ASP.NET Core 1.0 به همراه تعدادی Middleware توکار است؛ مانند:
- Authentication: جهت پشتیبانی از اعتبارسنجی
- CORS: برای فعال سازی Cross-Origin Resource Sharing
- Routing: جهت تعریف و محدود سازی مسیریابی‌های برنامه
- Session: برای پشتیبانی و مدیریت سشن‌های کاربران
- Diagnostics: پشتیبانی از صفحات خطا و اطلاعات زمان اجرای برنامه
و مواردی دیگر که برای توزیع فایل‌های استاتیک و یا مرور محتویات پوشه‌ها و امثال آن‌ها طراحی شده‌اند که در طی این مطلب و همچنین مطالب آتی، آن‌ها را بررسی خواهیم کرد.

یک مثال: اگر یک پروژه‌ی خالی ASP.NET Core 1.0 را در ویژوال استودیو ایجاد کنید، این پروژه قادر نیست حتی فایل‌های استاتیک مانند تصاویر، فایل‌های پیش فرض مانند index.html و یا قابلیت مرور ساده‌ی فایل‌های موجود در یک پوشه را ارائه دهد (حتی اگر به آن‌ها نیازی نداشته باشید).
طراحی این نگارش از ASP.NET، مبتنی است بر سبک وزن بودن و ماژولار بودن. هر قابلیتی را که نیاز دارید، باید middleware آن‌را نیز خودتان به صورت صریح اضافه کنید و یا در غیر اینصورت فعال نیست. برخلاف نگارش‌های قبلی ASP.NET که HTTP Module‌های از سشن گرفته تا اعتبارسنجی و غیره، همگی به صورت پیش فرض در فایل Web.Config فعال بودند؛ مگر اینکه آن‌ها را به صورت دستی حذف می‌کردید.



ثبت و فعال سازی اولین Middleware

در ادامه‌ی تکمیل و معرفی برنامه‌ی خالی ASP.NET Core 1.0، قصد داریم یک Middleware توکار را به نام Welcome Page، بجای نمایش سطر Hello world پیش فرض این برنامه، ثبت و فعال سازی کنیم. این Middleware ویژه، در اسمبلی به نام Microsoft.AspNetCore.Diagnostics قرار دارد. اگر فایل project.json پیش فرض این پروژه را باز کنید، این اسمبلی به صورت پیش فرض در آن ثبت و معرفی شده‌است:
{
    "dependencies": {
        "Microsoft.NETCore.App": {
            "version": "1.0.0",
            "type": "platform"
        },
        "Microsoft.AspNetCore.Diagnostics": "1.0.0",
        "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
        "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
        "Microsoft.Extensions.Logging.Console": "1.0.0"
    },
بنابراین هم اکنون بسته‌ی نیوگت آن نیز به پروژه‌ی جاری اضافه شده و قابل استفاده است. در غیراینصورت همانطور که در قسمت قبل در مطلب «نقش فایل project.json» عنوان شد، می‌توان بر روی گره references کلیک راست کرده و سپس از منوی ظاهر شده،‌گزینه‌ی manage nuget packages را انتخاب کرد. سپس، ابتدا برگه‌ی browse را انتخاب کنید و در اینجا نام Microsoft.AspNetCore.Diagnostics را جستجو کرده و در آخر آن‌را نصب کنید.
پس از اطمینان حاصل کردن از نصب بسته‌ی نیوگت Microsoft.AspNetCore.Diagnostics، اکنون جهت معرفی Middleware توکار Welcome Page ، فایل Startup.cs را گشوده و کدهای آن‌را به نحو ذیل تغییر  دهید:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
 
namespace Core1RtmEmptyTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }
 
        public void Configure(IApplicationBuilder app)
        {
            app.UseWelcomePage();
 
            app.Run(async (context) =>
            {
                await context.Response.WriteAsync("Hello DNT!");
            });
        }
    }
}
در اینجا نحوه‌ی ثبت این middleware را با افزودن آن به IApplicationBuilder تزریق شده‌ی توسط OWIN، به کمک متد الحاقی UseWelcomePage مشاهده می‌کنید.
به علاوه middleware دومی را نیز با متد الحاقی Run مشاهده می‌کنید. به این نوع middlewareهای خاص، اصطلاحا terminal middleware می‌گویند. از این جهت که درخواستی را دریافت و یک response را تولید می‌کنند و کار همینجا خاتمه می‌یابد و زنجیره‌ی پردازشی middlewareها ادامه نخواهد یافت. در اینجا پارامتر context آن از نوع HttpContext است و باید دقت داشت، زمانیکه کار نوشتن در Response، در اینجا انجام شد، اگر پس از متد Run یک Middleware دیگر را ثبت کنید، هیچگاه اجرا نخواهد شد.

در این حالت اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:


welcome page نیز یک terminal middleware است. به همین جهت middleware بعدی ثبت شده‌ی در اینجا یا همان متد Run، دیگر اجرا نخواهد شد.

در قسمت‌های بعدی، تعداد بیشتری از Middlewareهای توکار ASP.NET Core 1.0 را بررسی خواهیم کرد.
مطالب
یکسان سازی ی و ک دریافتی حین استفاده از NHibernate


تصویر فوق، یکی از تصویرهایی است که شاید از طریق ایمیل‌هایی تحت عنوان "فقط در ایران!" به دست شما هم رسیده باشد. تصور کاربر نهایی (که این ایمیل را با تعجب ارسال کرده) این است که در اینجا به او گفته شده مثلا "مرتضی" را جستجو نکنید و امثال آن. چون برای او تفاوتی بین ی و ی وجود ندارد. همچنین بکار بردن "اقلامی" هم کمی غلط انداز است و بیشتر ذهن را به سمت کلمه سوق می‌دهد تا حرف.

در ادامه‌ی بحث آلرژی مزمن به وجود انواع "ی" و "ک" در بانک اطلاعاتی (+ و + و +)، اینبار قصد داریم این اطلاعات را به NHibernate بسط دهیم. شاید یک روش اعمال یک دست سازی "ی" و "ک" این باشد که در کل برنامه هر جایی که قرار است update یا insert ایی صورت گیرد، خواص رشته‌ای را یافته و تغییر دهیم. این روش "کار می‌کنه" ولی ایده آل نیست؛ چون حجم کار تکراری در برنامه زیاد خواهد شد و نگهداری آن هم مشکل می‌شود. همچنین امکان فراموش کردن اعمال آن هم وجود دارد.
در NHibernate یک سری EventListener وجود دارند که کارشان گوش فرا دادن به یک سری رخدادها مانند مثلا update یا insert است. این رخدادها می‌توانند پیش یا پس از هرگونه ثبت یا ویرایشی در برنامه صادر شوند. بنابراین بهترین جایی که جهت اعمال این نوع ممیزی (Auditing) بدون بالا بردن حجم برنامه یا اضافه کردن بیش از حد یک سری کد تکراری در حین کار با NHibernate می‌توان یافت، روال‌های مدیریت کننده‌ی همین EventListener ها هستند.

کلاس YeKeAuditorEventListener نهایی با پیاده سازی IPreInsertEventListener و IPreUpdateEventListenerبه شکل زیر خواهد بود:
using NHibernate.Event;

namespace NHYeKeAuditor
{
public class YeKeAuditorEventListener : IPreInsertEventListener, IPreUpdateEventListener
{
// Represents a pre-insert event, which occurs just prior to performing the
// insert of an entity into the database.
public bool OnPreInsert(PreInsertEvent preInsertEvent)
{
var entity = preInsertEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}

// Represents a pre-update event, which occurs just prior to performing the
// update of an entity in the database.
public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
{
var entity = preUpdateEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}
}
}
در کدهای فوق روال‌های OnPreInsert و OnPreUpdate پیش از ثبت و ویرایش اطلاعات فراخوانی می‌شوند (همواره و بدون نیاز به نگرانی از فراموش شدن فراخوانی کدهای مربوطه). اینجا است که فرصت داریم تا تغییرات مورد نظر خود را جهت یکسان سازی "ی" و "ک" دریافتی اعمال کنیم (کد کلاس CorrectYeKe را در پیوست خواهید یافت).

تا اینجا فقط تعریف YeKeAuditorEventListener انجام شده است. اما NHibernate چگونه از وجود آن مطلع خواهد شد؟
برای تزریق کلاس YeKeAuditorEventListener به تنظیمات برنامه باید به شکل زیر عمل کرد:
using System;
using System.Linq;
using FluentNHibernate.Cfg;
using NHibernate.Cfg;

namespace NHYeKeAuditor
{
public static class MappingsConfiguration
{
public static FluentConfiguration InjectYeKeAuditorEventListener(this FluentConfiguration fc)
{
return fc.ExposeConfiguration(configListeners());
}

private static Action<Configuration> configListeners()
{
return
c =>
{
var listener = new YeKeAuditorEventListener();
c.EventListeners.PreInsertEventListeners =
c.EventListeners.PreInsertEventListeners
.Concat(new[] { listener })
.ToArray();
c.EventListeners.PreUpdateEventListeners =
c.EventListeners.PreUpdateEventListeners
.Concat(new[] { listener })
.ToArray();
};
}
}
}
به این معنا که FluentConfiguration خود را همانند قبل ایجاد کنید. درست در زمان پایان کار تنها کافی است متد InjectYeKeAuditorEventListener فوق بر روی آن اعمال گردد و بس (یعنی پیش از فراخوانی BuildSessionFactory).

کدهای NHYeKeAuditor را از اینجا می‌توانید دریافت کنید.

مطالب
Blazor 5x - قسمت ششم - مبانی Blazor - بخش 3 - چرخه‌های حیات کامپوننت‌ها
در این قسمت می‌خواهیم انواع رویدادهای چرخه‌ی حیات یک کامپوننت را بررسی کنیم. به همین جهت ابتدا دو کامپوننت جدید Lifecycle.razor و Lifecycle‍Child.razor را به مثالی که تا این قسمت تکمیل کرده‌ایم، اضافه کرده و آن‌ها‌را به صورت زیر جهت نمایش رویدادهای چرخه‌ی حیات، تغییر می‌دهیم:

کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle"
@using System.Threading

<div class="border">
    <h3>Lifecycles Parent Component</h3>

    <div class="border">
        <LifecycleChild CountValue="CurrentCount"></LifecycleChild>
    </div>

    <p>Current count: @CurrentCount</p>

    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
    <br /><br />
    <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount
</div>

@code
{
    int CurrentCount = 0;
    int MaxCount = 5;

    private void IncrementCount()
    {
        CurrentCount++;
        Console.WriteLine("Parnet - IncrementCount is called");
    }

    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("Parnet - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("Parnet - ShouldRender is called");
        return true;
    }

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}

و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\Lifecycle‍Child.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3>

@code
{
    [Parameter]
    public int CountValue { get; set; }

    protected override void OnInitialized()
    {
        Console.WriteLine("  Child - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnInitializedAsync is called");
    }

    protected override void OnParametersSet()
    {
        Console.WriteLine("  Child - OnParameterSet is called");
    }

    protected override async Task OnParametersSetAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnParametersSetAsync is called");
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == true) is called");
        }
        else
        {
            Console.WriteLine("  Child - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("  Child - OnAfterRenderAsync is called");
    }

    protected override bool ShouldRender()
    {
        Console.WriteLine("  Child - ShouldRender is called");
        return true;
    }
}
و همچنین برای دسترسی به آن‌ها، مدخل منوی زیر را به کامپوننت Shared\NavMenu.razor اضافه می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="lifecycle">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles
    </NavLink>
</li>
با توجه به اینکه برنامه‌ی جاری از نوع Blazor Server است، Console.WriteLine‌های آن، در صفحه‌ی کنسول اجرای برنامه ظاهر می‌شوند و نه در developer tools مرورگر:





رویدادهای OnInitialized و OnInitializedAsync

@code
{
    protected override void OnInitialized()
    {
        Console.WriteLine("Parnet - OnInitialized is called");
    }

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnInitializedAsync is called");
    }
همانطور که در تصویر فوق نیز ملاحظه می‌کنید، اولین رویدادی که فراخوانی می‌شود، OnInitialized نام دارد و پس از آن نمونه‌ی async آن به نام OnInitializedAsync. این رویدادها زمانیکه یک کامپوننت و اجزای UI آن کاملا بارگذاری شده‌اند، فراخوانی می‌شوند. مهم‌ترین کاربرد آن‌ها، دریافت اطلاعات از سرویس‌های برنامه‌است.
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام Lifecycle‍Child.razor فراخوانی شده‌است. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شده‌است و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده می‌کنیم.


رویدادهای OnParametersSet و OnParametersSetAsync

این رویدادها یکبار در زمان بارگذاری اولیه‌ی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت می‌کند، فراخوانی می‌شوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت می‌کند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
هرچند این پارامتر در UI کامپوننت فرزند مثال تهیه شده استفاده نمی‌شود، اما مقدار دهی آن از طرف والد، سبب بروز رویدادهای OnParametersSet و OnParametersSetAsync خواهد شد. برای آزمایش آن اگر بر روی دکمه‌ی click me در کامپوننت والد کلیک کنیم، این رویدادهای جدید را مشاهده خواهیم کرد:
Parnet - IncrementCount is called
Parnet - ShouldRender is called
  Child - OnParameterSet is called
  Child - ShouldRender is called
Parnet - OnAfterRender(firstRender == false) is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnParametersSetAsync is called
  Child - ShouldRender is called
  Child - OnAfterRender(firstRender == false) is called
  Child - OnAfterRenderAsync is called
Parnet - OnAfterRenderAsync is called
  Child - OnAfterRenderAsync is called
ابتدا متد IncrementCount کامپوننت والد فراخوانی شده‌است که سبب تغییر مقدار پارامتر CurrentCount ارسالی به کامپوننت Lifecycle‍Child می‌شود و پس از آن، رویداد OnParameterSet کامپوننت فرزند را مشاهده می‌کنید که عکس العملی است به این تغییر مقدار. یکی از کاربردهای آن، دریافت مقدار جدید پارامترهای کامپوننت و سپس به روز رسانی قسمت خاصی از UI بر اساس آن‌ها است.



رویدادهای OnAfterRender و OnAfterRenderAsync

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

یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمی‌شوند؛ چون در مرحله‌ی آخر رندر UI قرار دارند.

@code
{
    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called");
            CurrentCount = 111;
        }
        else
        {
            CurrentCount = 999;
            Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called");
        }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await Task.Delay(100);
        Console.WriteLine("Parnet - OnAfterRenderAsync is called");
    }
}
در مثال‌های فوق، پارامتر firstRender را نیز مشاهده می‌کنید. یک کامپوننت چندین بار می‌تواند رندر شود. برای مثال هربار که توسط رویدادگردانی مقدار فیلدی را که در UI استفاده می‌شود، تغییر دهیم، کامپوننت مجددا رندر می‌شود. برای نمونه با کلیک بر روی دکمه‌ی click me، سبب تغییر مقدار فیلد CurrentCount می‌شویم. این تغییر و فراخوانی ضمنی StateHasChanged در پایان کار متد و در پشت صحنه، سبب رندر مجدد UI شده و در نتیجه‌ی آن، مقدار جدیدی را در صفحه مشاهده می‌کنیم. در اینجا اگر خواستیم بدانیم که رندر انجام شده برای بار اول است که صورت می‌گیرد یا خیر، می‌توان از پارامتر firstRender استفاده کرد.

سؤال: با توجه به مقدار دهی‌های 111 و 999 صورت گرفته‌ی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده می‌شود؟
در اولین بار نمایش صفحه، لحظه‌ای عدد 111 و سپس عدد 999 نمایش داده می‌شود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، می‌توان مشاهده کرد.
اما ... اگر پس از نمایش اولیه‌ی صفحه، چندین بار بر روی دکمه‌ی click me کلیک کنیم، همواره عدد 1000 مشاهده می‌شود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمی‌شوند؛ چون در این مرحله، رندر UI به پایان رسیده‌است. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر می‌کند و به همین صورت باقی می‌ماند. دفعه‌ی بعدی که بر روی دکمه‌ی click me کلیک می‌کنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخ‌داد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده می‌کنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر می‌کند که ... دیگر به UI منعکس نمی‌شود تا زمان کلیک بعدی و همینطور الی آخر.


رویدادهای StateHasChanged و ShouldRender

- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازه‌ی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکان‌هایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیده‌است. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شده‌اند:
@page "/lifecycle"
@using System.Threading

button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount

@code
{
    int MaxCount = 5;

    void StartCountdown()
    {
        Console.WriteLine("Parnet - StartCountdown()");
        var timer = new Timer(TimeCallBack, null, 1000, 1000);
    }

    void TimeCallBack(object state)
    {
        if (MaxCount > 0)
        {
            MaxCount--;
            Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)");
            InvokeAsync(StateHasChanged);
        }
    }
}
تایمر تعریف شده، یک thread timer است. یعنی callback آن بر روی یک ترد جدید و مجزای از ترد UI اجرا می‌شود. در این حالت اگر StateHasChanged را جهت اطلاع رسانی تغییر حالت UI فراخوانی نکنیم، در حین کار تایمر، هیچ تغییری را در UI مشاهده نخواهیم کرد.


یک نکته: متدهای رویدادگردان در Blazor، می‌توانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز می‌شوند (به این دلیل است که با کلیک بر روی دکمه‌ای، UI هم به روز رسانی می‌شود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged می‌شوند؛ یکبار زمانیکه قسمت sync متد به پایان می‌رسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button>
<button @onclick="OnClickAsync">Asynchronous</button>
@code{
    void OnClick()
    {
    } // StateHasChanged is called after the method

    async Task OnClickAsync()
    {
        text = "click1";
        // StateHasChanged is called here as the synchronous part of the method ends

        await Task.Delay(1000);
        await Task.Delay(2000);
        text = "click2";
    } // StateHasChanged is called after the method
}
بنابراین یکی دیگر از دلایل نیاز به فراخوانی صریح InvokeAsync(StateHasChanged) در callback تایمر تعریف شده، عدم فراخوانی خودکار آن، در پایان کار رویداد callback تایمر است.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-06.zip
مطالب
ساخت یک گزارش ساز به کمک iTextSharp و Open Office

iTextSharp پایه کار با فایل‌های PDF را ارائه می‌دهد اما ابزاری را جهت ساده‌تر سازی تولید فایل‌های PDF به همراه ندارد؛ هر چند مثلا امکان تبدیل HTML به PDF را دارا است اما باید گفت: «تا حدودی البته». اگر نیاز باشد جدولی را ایجاد کنیم باید کد نویسی کرد، اگر نیاز باشد تصویری اضافه شود به همین ترتیب و الی آخر. البته این را هم باید در نظر داشت که کد نویسی انعطاف قابل توجهی را در اختیار برنامه نویس قرار می‌دهد؛ شاید به همین دلیل این روزها مباحث «Code first» بیشتر مورد توجه برنامه نویس‌ها است، تا مباحث «Wizard first» یک دهه قبل!
اما باز هم داشتن یک طراح بد نیست و می‌تواند در کاهش مدت زمان تولید نهایی یک فایل PDF مؤثر باشد. برای این منظور می‌توان از برنامه‌ی رایگان و معروف Open office استفاده کرد. توسط آن می‌توان یک فرم PDF را طراحی و سپس فیلدهای آن‌را (این قالب تهیه شده را) با iTextSharp پر کرد. این مورد می‌تواند برای تهیه گزارش‌هایی که تهیه آن‌ها با ابزارهای متداول گزارش سازی عموما میسر نیست،‌ بسیار مناسب باشد.


طراحی یک فرم PDF با استفاده از برنامه Open Office

آخرین نگارش برنامه Open office را از اینجا می‌توانید دریافت کنید و آنچنان حجمی هم ندارد؛ حدودا 154 مگابایت است.
پس از نصب و اجرای برنامه، حداقل به دو طریق می‌توان یک فرم جدید را شروع کرد:
الف) آغاز یک XML Form document جدید در Open office سبب خواهد شد که نوارهای ابزار طراحی فرم، مانند قرار دادن TextBox ، CheckBox و غیره به صورت خودکار ظاهر شوند.
ب) و یا آغاز یک سند معمولی و سپس مراجعه به منوی View->Toolbars->Form Controls هم همان حالت را به همراه خواهد داشت.


در اینجا برای طراحی یک گزارش یا فرم جدید تنها کافی است همانند روش‌های متداول تهیه یک سند معمولی رفتار کنیم و مواردی را که قرار است توسط iTextSharp مقدار دهی کنیم، با کنترل‌های نوار ابزار Form آن بر روی صفحه قرار دهیم که نمونه‌ی ساده آن‌را در شکل زیر ملاحظه می‌کنید:


برای گزارش‌های فارسی بهتر است Alignment یک کنترل به Right تنظیم شود و Border به حالت Without frame مقدار دهی گردد. نام این کنترل را هم بخاطر بسپارید و یا تغییر دهید. از این نام‌ها در iTextSharp استفاده خواهیم کرد. (صفحه خواص فوق با دوبار کلیک بر روی یک کنترل قرار گرفته بر روی فرم ظاهر می‌شود)

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


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


استفاده از iTextSharp جهت مقدار دهی فیلدهای یک فرم PDF

در ادامه می‌خواهیم این قالب گزارشی را که تهیه کردیم با کمک iTextSharp پر کرده و یک فایل PDF جدید تهیه کنیم. سورس کامل اینکار را در ذیل مشاهده می‌کنید:


using System;
using System.Diagnostics;
using System.IO;
using iTextSharp.text;
using iTextSharp.text.pdf;

namespace PdfForm
{
class Program
{
//روش صحیح ثبت و معرفی فونت در این کتابخانه
public static iTextSharp.text.Font GetTahoma()
{
var fontName = "Tahoma";
if (!FontFactory.IsRegistered(fontName))
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
FontFactory.Register(fontPath);
}
return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}

static void Main(string[] args)
{
string fileNameExisting = @"form.pdf";
string fileNameNew = @"newform.pdf";

using (var existingFileStream = new FileStream(fileNameExisting, FileMode.Open))
using (var newFileStream = new FileStream(fileNameNew, FileMode.Create))
{
var pdfReader = new PdfReader(existingFileStream);
using (var stamper = new PdfStamper(pdfReader, newFileStream))
{
//نکته مهم جهت کار با اطلاعات فارسی
//در غیراینصورت شاهد ثبت اطلاعات نخواهید بود
stamper.AcroFields.AddSubstitutionFont(GetTahoma().BaseFont);

//form.Fields.Keys = تمام فیلدهای موجود در فرم
var form = stamper.AcroFields;

//مقدار دهی فیلدهای فرم
form.SetField("TextBox1", "مقدار1");
form.SetField("TextBox2", "مقدار2");

// "Yes" and "Off" are valid values here
form.SetField("Check Box 1", "Yes");

// "" and "Off" are valid values here
form.SetField("Option Button 1", "");

// نحوه مقدار دهی لیست
form.SetListOption("ListBox1", new[] { "1مقدار یک", "مقدار دو1" }, null);
form.SetField("ListBox1", null);

// به این ترتیب فرم دیگر توسط کاربر قابل ویرایش نخواهد بود
//stamper.PartialFormFlattening --> جهت غیرقابل ویرایش نمودن فیلدی مشخص
stamper.FormFlattening = true;

stamper.Close();
pdfReader.Close();
}
}

Process.Start("newform.pdf");
}
}
}

توضیحات:
چون در اینجا فایل PDF، از پیش تهیه شده است، پس باید از اشیاء PdfReader و PdfStamper جهت خواندن و نوشتن اطلاعات در آن‌ها استفاده کرد. سپس توسط شیء stamper.AcroFields می‌توان به این فیلدها یا همان کنترل‌هایی که در برنامه‌ی Open office بر روی فرم قرار دادیم، دسترسی پیدا کنیم.
در ابتدا نیاز است فونت این فیلدها توسط متد AddSubstitutionFont مقدار دهی شود. این مورد برای گزارش‌های فارسی الزامی است؛ در غیراینصورت متنی را در خروجی مشاهده نخواهید کرد.
ادامه کار هم مشخص است. توسط متد form.SetField مقداری را به کنترل‌های قرار گرفته بر روی فرم نسبت می‌دهیم. آرگومان اول آن نام کنترل است و آرگومان دوم، مقدار مورد نظر می‌باشد. اگر کنترل CheckBox را بر روی صفحه قرار دادید، تنها مقدارهای Yes و Off را می‌پذیرد (آن هم با توجه به اینکه به کوچکی و بزرگی حروف حساس است). اگر یک Radio button یا در اینجا Option button را بر روی فرم قرار دادید، تنها مقدارهای خالی و Off را قبول خواهد کرد. نحوه‌ی مقدار دهی یک لیست هم در اینجا ذکر شده است.
در پایان چون نمی‌خواهیم کاربر نهایی قادر به ویرایش اطلاعات باشد، FormFlattening را true خواهیم کرد و به این ترتیب، کنترل‌ها فقط خواندنی خواهند شد. البته اگر همانطور که ذکر شد، border کنترل‌ها را در حین طراحی حذف کنید، PDF نهایی تولیدی یکپارچه و یک دست به نظر می‌رسد و اصلا مشخص نخواهد بود که این فایل پیشتر یک فرم قابل پر کردن بوده است.

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


معرفی ویژگی جدید StringSyntax

با استفاده از ویژگی StringSyntax جدید می‌توان مقدار مورد انتظار از رشته‌ی درخواستی را معنادار کرد. برای مثال، Visual Studio سال‌هاست که راهنمایی را در حین تعریف عبارات باقاعده ظاهر می‌کند. اما این راهنما صرفا مختص به ویژوال استودیو است و تا پیش از این راهی وجود نداشت تا عنوان کنیم که برای مثال این رشته قرار است تنها یک عبارت باقاعده باشد. اکنون در دات نت 7 با معرفی ویژگی جدید StringSyntax می‌توان یک چنین intellisense ای را در سایر IDEها نیز شاهد بود.
برای نمونه مثال زیر را درنظر بگیرید:
using System.Diagnostics.CodeAnalysis;

namespace CS11Tests;

public class StringSyntaxAttributeTests
{
    public static void Test()
    {
        RegexTest("");
        DateTest("");
    }

    private static void RegexTest([StringSyntax(StringSyntaxAttribute.Regex)] string regex)
    {
    }

    private static void DateTest([StringSyntax(StringSyntaxAttribute.DateTimeFormat)] string dateTime)
    {
    }
}
در اینجا با استفاده از ویژگی StringSyntax، دقیقا مشخص کرده‌ایم که هدف از تعریف پارامترهای رشته‌ای مدنظر چه چیزی بوده‌است. به این ترتیب، برای مثال در Rider، در حین استفاده از این متدها، به intellisense‌های زیر خواهیم رسید:

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


و راهنمای ظاهر شده جهت تعریف ساده‌تر یک DateTime:



امکان استفاده از StringSyntax در دات نت‌های پیش از نگارش 7

هرچند StringSyntax در دات نت 7 تعریف شده‌است؛ اما اگر تعریف کلاس زیر را به همراه فضای نام دقیق آن به پروژه‌های قدیمی‌تر هم اضافه کنیم ... برای دات نت‌های پیش از نگارش 7 هم کار می‌کند:
#if !NET7_0_OR_GREATER

namespace System.Diagnostics.CodeAnalysis
{
  /// <summary>Specifies the syntax used in a string.</summary>
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
  public sealed class StringSyntaxAttribute : Attribute
  {
    /// <summary>The syntax identifier for strings containing composite formats for string formatting.</summary>
    public const string CompositeFormat = "CompositeFormat";
    /// <summary>The syntax identifier for strings containing date format specifiers.</summary>
    public const string DateOnlyFormat = "DateOnlyFormat";
    /// <summary>The syntax identifier for strings containing date and time format specifiers.</summary>
    public const string DateTimeFormat = "DateTimeFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.Enum" /> format specifiers.</summary>
    public const string EnumFormat = "EnumFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.Guid" /> format specifiers.</summary>
    public const string GuidFormat = "GuidFormat";
    /// <summary>The syntax identifier for strings containing JavaScript Object Notation (JSON).</summary>
    public const string Json = "Json";
    /// <summary>The syntax identifier for strings containing numeric format specifiers.</summary>
    public const string NumericFormat = "NumericFormat";
    /// <summary>The syntax identifier for strings containing regular expressions.</summary>
    public const string Regex = "Regex";
    /// <summary>The syntax identifier for strings containing time format specifiers.</summary>
    public const string TimeOnlyFormat = "TimeOnlyFormat";
    /// <summary>The syntax identifier for strings containing <see cref="T:System.TimeSpan" /> format specifiers.</summary>
    public const string TimeSpanFormat = "TimeSpanFormat";
    /// <summary>The syntax identifier for strings containing URIs.</summary>
    public const string Uri = "Uri";
    /// <summary>The syntax identifier for strings containing XML.</summary>
    public const string Xml = "Xml";

    /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary>
    /// <param name="syntax">The syntax identifier.</param>
    public StringSyntaxAttribute(string syntax)
    {
      this.Syntax = syntax;
      this.Arguments = Array.Empty<object>();
    }

    /// <summary>Initializes the <see cref="T:System.Diagnostics.CodeAnalysis.StringSyntaxAttribute" /> with the identifier of the syntax used.</summary>
    /// <param name="syntax">The syntax identifier.</param>
    /// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
    public StringSyntaxAttribute(string syntax, params object?[] arguments)
    {
      this.Syntax = syntax;
      this.Arguments = arguments;
    }

    /// <summary>Gets the identifier of the syntax used.</summary>
    public string Syntax { get; }

    /// <summary>Gets the optional arguments associated with the specific syntax employed.</summary>
    public object?[] Arguments { get; }
  }
}

#endif
نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت دوم
- در همان تصویر اولی که ارسال کردید، برگه‌ی اول headers هست و برگه‌ی سوم آن response، که صفحه‌ی زرد رنگ معروف خطاهای ASP.NET را نمایش می‌دهد.
- ابتدا بررسی کنید response ایی که در حالت دیباگ مشاهده می‌کنید چی هست.
- سپس بررسی کنید اصلا متد RegisterRoutes ایی که عنوان شد، فراخوانی می‌شود و مسیریابی آن در سیستم ثبت می‌شود یا خیر؟ (یک break point داخل آن قرار دهید)
اگر فراخوانی نمی‌شود، بررسی کنید آیا فایل‌های پوشه‌ی bin این افزونه، به پوشه‌ی bin پروژه‌ی اصلی کپی شده‌اند یا خیر؟
مطالب
الگوی نماینده (پروکسی) Proxy Pattern
همه کاربران کامپیوتر در ایران به خوبی با کلمه پروکسی آشنا هستند. پروکسی به معنی نماینده یا واسط است و پروکسی واسطی است بین ما و شیء اصلی. پروکسی در شبکه به این معنی است که سیستم شما به یک سیستم واسط متصل شده است که از طریق پروکسی محدودیت‌های دسترسی برای آن تعریف شود. در اینجا هم پروکسی در واقع به همین منظور استفاده می‌شود. در تعدادی از کامنت‌های سایت خوانده بودم دوستان در مورد اصول SOLID  و Refactoring بحث می‌کردند که آیا انجام عمل اعتبارسنجی در خود اصل متد کار درستی است یا خیر. در واقع خودم هم نمی‌دانم که این حرکت چقدر به این اصول پایبند است یا خیر، ولی می‌دانم که الگوی پروکسی کل سؤالات بالا را حذف می‌کند و با این الگو دیگر این سؤال اهمیتی ندارد. به عنوان مثال فرض کنید که شما یک برنامه ساده کار با فایل را دارید. ولی اگر بخواهید اعتبارسنجی‌هایی را برای آن تعریف کنید، بهتر است اینکار را به یک پروکسی بسپارید تا شیء گرایی بهتری را داشته باشید.

برای شروع ابتدا یک اینترفیس تعریف می‌کنیم:
   public interface IFilesService
    {
        DirectoryInfo GetDirectoryInfo(string directoryPath);
        void DeleteFile(string fileName);
        void WritePersonInFile(string fileName,string name, string lastName, byte age);
    }
این اینترفیس شامل سه متد نام نویسی، حذف فایل و دریافت اطلاعات یک دایرکتوری است. بعد از آن دو عدد کلاس را از آن مشتق می‌کنیم:
کلاس اصلی:
    class FilesServices:IFilesService
    {
        public DirectoryInfo GetDirectoryInfo(string directoryPath)
        {
            return new DirectoryInfo(directoryPath);
        }

        public void DeleteFile(string fileName)
        {
            File.Delete(fileName);
            Console.WriteLine("the file has been deleted");
        }

        public void WritePersonInFile(string fileName, string name, string lastName, byte age)
        {
            var text = $"my name is {name} {lastName} with {age} years old from dotnettips.info";
            File.WriteAllText(fileName,text);
        }    
    }
که تنها وظیفه اجرای فرامین را دارد و دیگری کلاس پروکسی است که وظیف تامین اعتبارسنجی و آماده سازی پیش شرط‌ها را دارد:
    class FilesServicesProxy:IFilesService
    {
        private readonly IFilesService _filesService;

        public FilesServicesProxy()
        {
            _filesService=new FilesServices();
        }

        public DirectoryInfo GetDirectoryInfo(string directoryPath)
        {
            var existing = Directory.Exists(directoryPath);
            if (!existing)
                Directory.CreateDirectory(directoryPath);
            return _filesService.GetDirectoryInfo(directoryPath);
        }

        public void DeleteFile(string fileName)
        {
            if(!File.Exists(fileName))
                Console.WriteLine("Please enter a valid path");
            else
                _filesService.DeleteFile(fileName);
        }

        public void WritePersonInFile(string fileName, string name, string lastName, byte age)
        {
            if (!Directory.Exists(fileName.Remove(fileName.LastIndexOf("\\"))))
            {
                Console.WriteLine("File Path is not valid");
                return;
            }
            if (name.Trim().Length == 0)
            {
                Console.WriteLine("first name must enter");
                return;
            }

            if (lastName.Trim().Length == 0)
            {
                Console.WriteLine("last name must enter");
                return;
            }

            if (age<18)
            {
                Console.WriteLine("your age is illegal");
                return;
            }


            if (name.Trim().Length < 3)
            {
                Console.WriteLine("first name must be more than 2 letters");
                return;
            }

            if (lastName.Trim().Length <5)
            {
                Console.WriteLine("last name must be more than 4 letters");
                return;
            }

            _filesService.WritePersonInFile(fileName,name,lastName,age);
            Console.WriteLine("the file has been written");
        }
    }
کلاس پروکسی، همان کلاسی است که شما باید صدا بزنید. وظیفه کلاس پروکسی هم این است که در زمان معین و صحیح، کلاس اصلی شما را بعد از اعتبارسنجی‌ها و انجام پیش شرط‌ها صدا بزند. همانطور که می‌بیند، ما در سازنده کلاس اصلی را در حافظه قرار می‌دهیم. سپس در هر متد، اعتبارسنجی‌های لازم را انجام می‌دهیم. مثلا در متد GetDirectoryInfo باید اطلاعات دایرکتوری بازگشت داده شود؛ ولی اصل عمل در واقع در کلاس اصلی است و اینجا فقط شرط گذاشته‌ایم اگر مسیر داده شده معتبر نبود، همان مسیر را ایجاد کن و سپس متد اصلی را صدا بزن. یا اگر فایل موجود است جهت حذف آن اقدام کن و ...
در نهایت در بدنه اصلی با تست چندین حالت مختلف، همه متد‌ها را داریم:
static void Main(string[] args)
        {
            IFilesService filesService=new FilesServicesProxy();
            filesService.WritePersonInFile("c:\\myfakepath\\a.txt","ali","yeganeh",26);

            var directory = filesService.GetDirectoryInfo("d:\\myrightpath\\");
            var fileName = Path.Combine(directory.FullName, "dotnettips.txt");

            filesService.WritePersonInFile(fileName, "al", "yeganeh", 26);
            filesService.WritePersonInFile(fileName, "ali", "yeganeh", 12);
            filesService.WritePersonInFile(fileName, "ali", "yeganeh", 26);

            filesService.DeleteFile("c:\\myfakefile.txt");
            filesService.DeleteFile(fileName);
        }
و نتیجه خروجی:
File Path is not valid
first name must be more than 2 letters
your age is illegal
the file has been written
Please enter a valid path
the file has been deleted
مطالب
افزودن یک صفحه‌ی جدید و دریافت و نمایش اطلاعات از سرور به کمک Ember.js
در قسمت قبل با مقدمات برپایی یک برنامه‌ی تک صفحه‌ای وب مبتنی بر Ember.js آشنا شدیم. مثال انتهای بحث آن نیز یک لیست ساده را نمایش می‌دهد. در ادامه همین برنامه را جهت نمایش لیستی از اشیاء JSON دریافتی از سرور تغییر خواهیم داد. همچنین یک صفحه‌ی about را نیز به آن اضافه خواهیم کرد.


پیشنیازهای سمت سرور

- ابتدا یک پروژه‌ی خالی ASP.NET را ایجاد کنید. نوع آن مهم نیست که Web Forms باشد یا MVC.
- سپس قصد داریم مدل کاربران سیستم را توسط یک ASP.NET Web API Controller در اختیار Ember.js قرار دهیم. مباحث پایه‌ای Web API نیز در وب فرم‌ها و MVC یکی است.
مدل سمت سرور برنامه چنین شکلی را دارد:
namespace EmberJS02.Models
{
    public class User
    {
        public int Id { set; get; }
        public string UserName { set; get; }
        public string Email { set; get; }
    }
}
کنترلر Web API ایی که این اطلاعات را در ختیار کلاینت‌ها قرار می‌دهد، به نحو ذیل تعریف می‌شود:
using System.Collections.Generic;
using System.Web.Http;
using EmberJS02.Models;
 
namespace EmberJS02.Controllers
{
    public class UsersController : ApiController
    {
        // GET api/<controller>
        public IEnumerable<User> Get()
        {
            return UsersDataSource.UsersList;
        }
    }
}
در اینجا UsersDataSource.UsersList صرفا یک لیست جنریک ساده از کلاس User است و کدهای کامل آن‌را می‌توانید از فایل پیوست انتهای بحث دریافت کنید.

همچنین فرض بر این است که مسیریابی سمت سرور ذیل را نیز به فایل global.asax.cs، جهت فعال سازی دسترسی به متدهای کنترلر UsersController تعریف کرده‌اید:
using System;
using System.Web.Http;
using System.Web.Routing;
 
namespace EmberJS02
{
    public class Global : System.Web.HttpApplication
    { 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
        }
    }
}

پیشنیازهای سمت کاربر

پیشنیازهای سمت کاربر این قسمت با قسمت «تهیه‌ی اولین برنامه‌ی Ember.js» دقیقا یکی است.
ابتدا فایل‌های مورد نیاز Ember.js به برنامه اضافه شده‌اند:
 PM> Install-Package EmberJS
سپس یک فایل app.js با محتوای ذیل به پوشه‌ی Scripts اضافه شده‌است:
App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
    setupController:function(controller) {
        controller.set('content', ['red', 'yellow', 'blue']);
    }
});
و  در آخر یک فایل index.html با محتوای ذیل کار برپایی اولیه‌ی یک برنامه‌ی مبتنی بر Ember.js را انجام می‌دهد:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script src="Scripts/jquery-2.1.1.js" type="text/javascript"></script>
    <script src="Scripts/handlebars.js" type="text/javascript"></script>
    <script src="Scripts/ember.js" type="text/javascript"></script>
    <script src="Scripts/app.js" type="text/javascript"></script>
</head>
<body>
    <script type="text/x-handlebars" data-template-name="application">
        <h1>Header</h1>
        {{outlet}}
    </script>
    <script type="text/x-handlebars" data-template-name="index">
        Hello,
        <strong>Welcome to Ember.js</strong>!
        <ul>
            {{#each item in content}}
            <li>
                {{item}}
            </li>
            {{/each}}
        </ul>
    </script>
</body>
</html>
تا اینجا را در قسمت قبل مطالعه کرده بودید.
در ادامه قصد داریم به هدر صفحه، دو لینک Home و About را اضافه کنیم؛ به نحوی که لینک Home به مسیریابی index و لینک About به مسیریابی about که صفحه‌ی جدید «درباره‌ی برنامه» را نمایش می‌دهد، اشاره کنند.


تعریف صفحه‌ی جدید About

برنامه‌های Ember.js، برنامه‌های تک صفحه‌ای وب هستند و صفحات جدید در آن‌ها به صورت یک template جدید تعریف می‌شوند که نهایتا متناظر با یک مسیریابی مشخص خواهند بود.
به همین جهت ابتدا در فایل app.js مسیریابی about را اضافه خواهیم کرد:
App.Router.map(function() {
    this.resource('about');
});
به این ترتیب با فراخوانی آدرس about/ در مرورگر توسط کاربر، منابع مرتبط با این آدرس و قالب مخصوص آن، توسط Ember.js پردازش خواهند شد.
بنابراین به صفحه‌ی index.html برنامه مراجعه کرده و صفحه‌ی about را توسط یک قالب جدید تعریف می‌کنیم:
<script type="text/x-handlebars" data-template-name="about">
    <h2>Our about page</h2>
</script>
تنها نکته‌ی مهم در اینجا مقدار data-template-name است که سبب خواهد شد تا به مسیریابی about، به صورت خودکار متصل و مرتبط شود.

در این حالت اگر برنامه را در حالت معمولی اجرا کنید، خروجی خاصی را مشاهده نخواهید کرد. بنابراین نیاز است تا لینکی را جهت اشاره به این مسیر جدید به صفحه اضافه کنیم:
<script type="text/x-handlebars" data-template-name="application">
    <h1>Ember Demo App</h1>
    <ul class="nav">
        <li>{{#linkTo 'index'}}Home{{/linkTo}}</li>
        <li>{{#linkTo 'about'}}About{{/linkTo}}</li>
    </ul>
    {{outlet}}
</script>
اگر از قسمت قبل به خاطر داشته باشید، عنوان شد که قالب ویژه‌ی application به صورت خودکار با وهله سازی Ember.Application.create به صفحه اضافه می‌شود. اگر نیاز به سفارشی سازی آن وجود داشت، خصوصا جهت تعریف عناصری که باید در تمام صفحات حضور داشته باشند (مانند منوها)، می‌توان آن‌را به نحو فوق سفارشی سازی کرد.
در اینجا با استفاده از امکان یا directive ویژه‌ای به نام linkTo، لینک‌هایی به مسیریابی‌های index و about اضافه شده‌اند. به این ترتیب اگر کاربری برای مثال بر روی لینک About کلیک کند، کتابخانه‌ی Ember.js او را به صورت خودکار به مسیریابی about و سپس نمایش قالب مرتبط با آن (قالب about ایی که پیشتر تعریف کردیم) هدایت خواهد کرد؛ مانند تصویر ذیل:


همانطور که در آدرس صفحه نیز مشخص است، هرچند صفحه‌ی about نمایش داده شده‌است، اما هنوز نیز در همان صفحه‌ی اصلی برنامه قرار داریم. به علاوه در این قسمت جدید، همچنان منوی بالای صفحه نمایان است؛ از این جهت که تعاریف آن به قالب application اضافه شده‌اند.


دریافت و نمایش اطلاعات از سرور

اکنون که با نحوه‌ی تعریف یک صفحه‌ی جدید و برپایی سیم کشی‌های مرتبط با آن آشنا شدیم، می‌خواهیم صفحه‌ی دیگری را به نام Users به برنامه اضافه کنیم و در آن لیست کاربران ارائه شده توسط کنترلر Web API سمت سرور ابتدای بحث را نمایش دهیم.
بنابراین ابتدا مسیریابی جدید users را به صفحه اضافه می‌کنیم تا لیست کاربران، در آدرس users/ قابل دسترسی شود:
App.Router.map(function() {
    this.resource('about');
    this.resource('users');
});
سپس نیاز است مدلی را توسط فراخوانی Ember.Object.extend ایجاد کرده و به کمک متد reopenClass آن‌را توسعه دهیم:
App.UsersLink = Ember.Object.extend({});
App.UsersLink.reopenClass({
    findAll: function () {
        var users = [];
        $.getJSON('/api/users').then(function(response) {
            response.forEach(function(item) {
                users.pushObject(App.UsersLink.create(item));
            });
        });
        return users;
    }
});
در اینجا متد دلخواهی را به نام findAll اضافه کرده‌ایم که توسط متد getJSON جی‌کوئری، به مسیر /api/users سمت سرور متصل شده و لیست کاربران را از سرور به صورت JSON دریافت می‌کند. در اینجا خروجی دریافتی از سرور به کمک متد pushObject به آرایه کاربران اضافه خواهد شد. همچنین نحوه‌ی فراخوانی متد create مدل UsersLink را نیز در اینجا مشاهده می‌کنید (App.UsersLink.create).

پس از اینکه نحوه‌ی دریافت اطلاعات از سرور مشخص شد، باید اطلاعات این مدل را در اختیار مسیریابی Users قرار داد:
App.UsersRoute = Ember.Route.extend({
    model: function() {
        return App.UsersLink.findAll();
    }
});
 
App.UsersController = Ember.ObjectController.extend({
    customHeader : 'Our Users List'
});
به این ترتیب زمانیکه کاربر به مسیر users/ مراجعه می‌کند، سیستم مسیریابی می‌داند که اطلاعات مدل خود را باید از کجا تهیه نماید.
همچنین در کنترلری که تعریف شده، صرفا یک خاصیت سفارشی و دلخواه جدید، به نام customHeader برای نمایش در ابتدای صفحه تعریف و مقدار دهی گردیده‌است.
اکنون قالبی که قرار است اطلاعات مدل را نمایش دهد، چنین شکلی را خواهد داشت:
<script type="text/x-handlebars" data-template-name="users">
    <h2>{{customHeader}}</h2>
    <ul>
        {{#each item in model}}
        <li>
            {{item.Id}}-{{item.UserName}} ({{item.Email}})
        </li>
        {{/each}}
    </ul>
</script>
با تنظیم data-template-name به users سبب خواهیم شد تا این قالب اطلاعات خودش را از مسیریابی users دریافت کند. سپس یک حلقه نوشته‌ایم تا کلیه عناصر موجود در مدل را خوانده و در صفحه نمایش دهد. همچنین در عنوان قالب نیز از خاصیت سفارشی customHeader استفاده شده‌است:




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