مطالب
آشنایی با جنریک‌ها #1
طبق این معرفی ، جنریک‌ها باعث می‌شوند که نوع داده‌ای (data type) المان‌های برنامه در زمان استفاده از آن‌ها در برنامه مشخص شوند. به عبارت دیگر، جنریک به ما اجازه می‌دهد کلاس‌ها یا متدهایی بنویسیم که می‌توانند با هر نوع داده‌ای کار کنند.

نکاتی از جنریک‌ها:
  • برای به حداکثر رسانی استفاده مجدد از کد، type safety و کارایی است.
  • بیشترین استفاده مشترک از جنریک‌ها جهت ساختن کالکشن کلاس‌ها (collection classes) است.
  • تا حد ممکن از جنریک کالکشن کلاسها (generic collection classes) جدید فضای نام System.Collections.Generic بجای کلاس‌هایی مانند ArrayList در فضای نام System.Collections استفاده شود.
  • شما می‌توانید اینترفیس جنریک ، کلاس جنریک ، متد جنریک و عامل جنریک سفارشی خودتان تهیه کنید.
  • جنریک کلاس‌ها، ممکن است در دسترسی به متدهایی با نوع داده‌ای خاص محدود شود.
  • بوسیله reflection، می‌توانید اطلاعاتی که در یک جنریک در زمان اجرا (run-time) قرار دارد بدست آورید.
انواع جنریک ها:
  1. کلاس‌های جنریک
  2. اینترفیس‌های جنریک
  3. متدهای جنریک
  4. عامل‌های جنریک
در قسمت اول به معرفی کلاس جنریک می‌پردازیم.

کلاس‌های جنریک
کلاس جنریک یعنی کلاسی که می‌تواند با چندین نوع داده کار کند برای آشنایی با این نوع کلاس به کد زیر دقت کنید:
using System;
using System.Collections.Generic;

namespace GenericApplication
{
    public class MyGenericArray<T>
    {
        // تعریف یک آرایه از نوع جنریک
        private T[] array;

        public MyGenericArray(int size)
        {
            array = new T[size + 1];
        }

        // بدست آوردن یک آیتم جنریک از آرایه جنریک
        public T getItem(int index)
        {
            return array[index];
        }

        // افزودن یک آیتم جنریک به آرایه جنریک
        public void setItem(int index, T value)
        {
            array[index] = value;
        }
    }
}
در کد بالا کلاسی تعریف شده است که می‌تواند بر روی آرایه‌هایی از نوع داده‌ای مختلف عملیات درج و حذف را انجام دهد. برای تعریف کلاس جنریک کافی است عبارت <T> بعد از نام کلاس خود اضافه کنید، سپس همانند سایر کلاس‌ها از این نوع داده ای در کلاس استفاده کنید. در مثال بالا یک آرایه از نوع T تعریف شده است که این نوع، در زمان استفاده مشخص خواهد شد. (یعنی در زمان استفاده از کلاس مشخص خواهد شد که چه نوع آرایه ای ایجاد می‌شود)
در کد زیر نحوه استفاده از کلاس جنریک نشان داده شده است، همانطور که مشاهده می‌کنید نوع کلاس int و char در نظر گرفته شده است (نوع کلاس، زمان استفاده از کلاس مشخص می‌شود) و سپس آرایه هایی از نوع int و char ایجاد شده است و 5 آیتم از نوع int و char به آرایه‌های هم نوع افزوده شده است. 
class Tester
{
        static void Main(string[] args)
        {
            // تعریف یک آرایه از نوع عدد صحیح
            MyGenericArray<int> intArray = new MyGenericArray<int>(5);

            // افزودن اعداد صحیح به آرایه ای از نوع عدد صحیح
            for (int c = 0; c < 5; c++)
            {
                intArray.setItem(c, c*5);
            }

            // بدست آوردن آیتم‌های آرایه ای از نوع عدد صحیح
            for (int c = 0; c < 5; c++)
            {
                Console.Write(intArray.getItem(c) + " ");
            }
            Console.WriteLine();

            // تعریف یک آرایه از نوع کاراکتر
            MyGenericArray<char> charArray = new MyGenericArray<char>(5);

            // افزودن کاراکترها به آرایه ای از نوع کاراکتر
            for (int c = 0; c < 5; c++)
            {
                charArray.setItem(c, (char)(c+97));
            }

            // بدست آوردن آیتم‌های آرایه ای از نوع کاراکتر
            for (int c = 0; c< 5; c++)
            {
                Console.Write(charArray.getItem(c) + " ");
            }
            Console.WriteLine();
            Console.ReadKey();
        }
}
زمانی که کد بالا اجرا می‌شود خروجی زیر بدست می‌آید:
0 5 10 15 20
a b c d e
نظرات مطالب
یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017
فایل zip پیوستی انتهای بحث را دریافت کنید. در فایل src\app\app.module.ts آن، تعریف ذیل برای معرفی HttpModule وجود دارد و بدون آن، استثنای No provider for Http را دریافت خواهید کرد:
import { HttpModule } from '@angular/http';

@NgModule({
  imports: [
    HttpModule
  ]
مطالب
Vue.js - نصب و راه‌اندازی اولیه - قسمت اول
Vue.js چیست؟
ویو‌جی‌اس یا وو جی اس (Vue.js) یکی از فریم‌ورک‌های جاوااسکریپتی است که به ما در ساختن اپلیکیشن‌های تحت وب و تحت معماری MVVM یا همان (Model–view–viewmodel) کمک می‌کند.
در حالیکه مدت کمی از انتشار Vue.js می‌گذرد، اما این فریم‌ورک به شدت محبوب شده و طرفداران زیادی پیدا کرده است.

پیش‌نیازها :
  • آشنایی کامل با جاوااسکریپت
  • آشنایی با Object-oriented در جاوااسکریپت 

اولین قدم برای استفاده از Vue.js، نصب آن می‌باشد که مرحله به مرحله توضیح خواهم داد. جهت نصب، سایت Vue.js به ما چندین راه پیشنهاد می‌دهد که ما آنها را در ذیل بررسی می‌کنیم:
1- CDN
در قسمت CDN به آدرس زیر مراجعه کرده و لینک آن را در صفحه‌ای که نیاز است از آن استفاده کنیم، قرار می‌دهیم. هرچندکه به صورت لوکال هم می‌توانید کدها را در یک فایل ذخیره کرده و از آن استفاده کنید.
ابتدا باید آدرس زیر، درون تگ < script> قرار بگیرد
https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script>

2- NPM
طبق همین دستور می‌توانید Vue.js را نصب کنید 
$ npm install vue

3- CLI 
 با استفاده از کامند لاین هم به راحتی قابل نصب می‌باشد؛ با استفاده از دستورات زیر
$ npm install -g vue-cli
$ vue init webpack my-project
$ cd my-project
$ npm install
$ npm run dev

توجه: بهترین روش نصب، استفاده از CDN می‌باشد که توصیه می‌شود جهت سهولت و جلوگیری از دردسرهای نصب فریم‌ورک، از مورد اول استفاده کنید.
حال پس از نصب، چنین کدی آماده خواهد شد.
<html> 
    <body> 

      <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script> 
    </body> 
</html>
آخرین نسخه از ویو جی اس، 2.2.0 است.
مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
در طی چند قسمت، نحوه‌ی طراحی یک سیستم افزونه پذیر را با ASP.NET MVC بررسی خواهیم کرد. عناوین مواردی که در این سری پیاده سازی خواهند شد به ترتیب ذیل هستند:
1- چگونه Area‌های استاندارد را تبدیل به یک افزونه‌ی مجزا و منتقل شده‌ی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایه‌ای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگی‌ها تا ثبت مسیریابی‌ها و امثال آن تدارک ببینیم.
3- چگونه فایل‌های CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائه‌ی مجزای آ‌ن‌ها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدل‌ها و موجودیت‌های خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.


در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژه‌های ASP.NET MVC بدون ارائه فایل‌های View آن
ج) آشنایی با تزریق وابستگی‌ها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build


تبدیل یک Area به یک افزونه‌ی مستقل

روش‌های زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آن‌ها در اسمبلی‌های دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری می‌شود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژه‌ی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونه‌ی Razor Generator را نصب کنید.
2) سپس یک پروژه‌ی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شده‌است.
3) در ادامه یک پروژه‌ی معمولی دیگر ASP.NET MVC را نیز به پروژه‌ی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شده‌است.
4) به پروژه‌ی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژه‌ی افزونه، تمام پوشه‌های غیر Area را حذف کنید. پوشه‌های Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آن‌را نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت می‌کند و در این حالت دیگر نیازی به پوشه‌های Controllers و Models و Views واقع شده در ریشه‌ی اصلی پروژه‌ی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
  PM> Install-Package RazorGenerator.Mvc
این دستور را باید یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه، اجرا کنید.


همانطور که در تصویر نیز مشخص شده‌است، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژه‌ی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژه‌ی اصلی و افزونه است:
  PM> Enable-RazorGenerator
بدیهی است این دستور را نیز باید همانند تصویر فوق، یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه اجرا کنید.
همچنین هربار که View جدیدی اضافه می‌شود نیز باید این‌کار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آن‌را به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator این‌کار را به صورت خودکار انجام می‌دهد.


تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشه‌ی bin پروژه‌ی اصلی، دیگر نیازی به ارائه‌ی فایل‌های View آن نیست و تمام اطلاعات کنترلرها، مدل‌ها و Viewها به صورت یکجا از فایل dll افزونه‌ی ارائه شده خوانده می‌شوند.


کپی کردن خودکار افزونه به پوشه‌ی Bin پروژه‌ی اصلی

پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونه‌ها)، نیاز است فایل‌های پوشه‌ی bin آن‌را به پوشه‌ی bin پروژه‌ی اصلی کپی کنیم (پروژه‌ی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونه‌ی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژه‌ی افزونه مراجعه کرده و قسمت Build events آن‌را به نحو ذیل تنظیم کنید:


در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
 Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
و سبب خواهد شد تا پس از هر کامپایل موفق، فایل‌های اسمبلی افزونه به پوشه‌ی bin پروژه‌ی MvcPluginMasterApp به صورت خودکار کپی شوند.


تنظیم فضاهای نام کلیه مسیریابی‌های پروژه

در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونه‌ی کپی شده به پوشه‌ی bin را دریافت و به Application domain جاری اعمال می‌کند؛ برای اینکار نیازی به کد نویسی اضافه‌تری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت می‌کنید. خطاهای حاصل عنوان می‌کند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژه‌ی اصلی و دیگری در پروژه‌ی افزونه و مشخص نیست که مسیریابی‌ها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژه‌ی افزونه مراجعه کرده و مسیریابی آن‌را به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc;
 
namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea
{
    public class NewsAreaAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "NewsArea";
            }
        }
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "NewsArea_default",
                "NewsArea/{controller}/{action}/{id}",
                // تکمیل نام کنترلر پیش فرض
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) }
            );
        }
    }
}
همینکار را باید در پروژه‌ی اصلی و هر پروژه‌ی افزونه‌ی جدیدی نیز تکرار کرد. برای مثال به فایل RouteConfig.cs پروژه‌ی اصلی مراجعه کرده و تنظیم ذیل را اعمال نمائید:
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcPluginMasterApp
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) }
            );
        }
    }
}
بدون تنظیم فضاهای نام هر مسیریابی، امکان استفاده‌ی بهینه و بدون خطا از Areaها وجود نخواهد داشت.


طراحی قرارداد پایه افزونه‌ها

تا اینجا با نحوه‌ی تشکیل ساختار هر پروژه‌ی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و  امثال آن نیز خواهد داشت. به همین منظور، یک پروژه‌ی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System;
using System.Reflection;
using System.Web.Optimization;
using System.Web.Routing;
using StructureMap;
 
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        MenuItem GetMenuItem(RequestContext requestContext);
        void RegisterBundles(BundleCollection bundles);
        void RegisterRoutes(RouteCollection routes);
        void RegisterServices(IContainer container);
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        //public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    }
 
    public class MenuItem
    {
        public string Name { set; get; }
        public string Url { set; get; }
    }
}
پروژه‌ی این قرارداد برای کامپایل شدن، نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package Microsoft.AspNet.Web.Optimization
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.


توضیحات قرار داد IPlugin

از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق می‌شود. برای مثال فعلا کلاس ذیل را به افزونه‌ی پروژه اضافه نمائید:
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
 
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            //todo: ...
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
        }
 
        public void RegisterServices(IContainer container)
        {
            // todo: add custom services.
 
            container.Configure(cfg =>
            {
                //cfg.For<INewsService>().Use<EfNewsService>();
            });
        }
    }
}
در قسمت جاری فقط از متد GetMenuItem آن استفاده خواهیم کرد. در قسمت‌های بعد، تنظیمات EF، تنظیمات مسیریابی‌ها و Bundling و همچنین ثبت سرویس‌های افزونه را نیز بررسی خواهیم کرد.
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحه‌ی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوه‌ی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Area‌ایی به نام NewsArea، مشاهده می‌کنید.


بارگذاری و تشخیص خودکار افزونه‌ها

پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آن‌ها را به صورت خودکار یافته و سپس پردازش کنیم. این‌کار را به کتابخانه‌ی StructureMap واگذار خواهیم کرد. برای این منظور پروژه‌ی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آن‌را به نحو ذیل تغییر دهید:
using System;
using System.IO;
using System.Threading;
using System.Web;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
using StructureMap.Graph;
 
namespace MvcPluginMasterApp.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(cfg =>
            {
                cfg.Scan(scanner =>
                {
                    scanner.AssembliesFromPath(
                        path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"),
                            // یک اسمبلی نباید دوبار بارگذاری شود
                        assemblyFilter: assembly =>
                        {
                            return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName);
                        });
 
                    scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically.
                    scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName);
                });
            });
        }
    }
}
این پروژه‌ی class library جدید برای کامپایل شدن نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.

کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشه‌ی Bin پروژه‌ی اصلی به structuremap معرفی شده‌است. سپس به آن گفته‌ایم که تنها اسمبلی‌هایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوع‌های IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.


تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار

در ادامه به پروژه‌ی اصلی مراجعه کرده و در پوشه‌ی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp;
using MvcPluginMasterApp.IoCConfig;
using MvcPluginMasterApp.PluginsBase;
 
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")]
 
namespace MvcPluginMasterApp
{
    public static class PluginsStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            foreach (var plugin in plugins)
            {
                plugin.RegisterServices(SmObjectFactory.Container);
                plugin.RegisterRoutes(RouteTable.Routes);
                plugin.RegisterBundles(BundleTable.Bundles);
            }
        }
    }
}
بدیهی است در این حالت نیاز است ارجاعی را به پروژه‌ی MvcPluginMasterApp.PluginsBase به پروژه‌ی اصلی اضافه کنیم.
دراینجا با استفاده از کتابخانه‌ای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شده‌است)، یک متد PostApplicationStartMethod سفارشی را تعریف کرده‌ایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار می‌دادیم.
در اینجا با استفاده از structuremap، تمام افزونه‌های موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آن‌ها به هر افزونه به صورت مستقل، تزریق خواهد شد.


اضافه کردن منو‌های خودکار افزونه‌ها به پروژه‌ی اصلی

پس از اینکه کار پردازش اولیه‌ی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشه‌ی shared پروژه‌ی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig
@using MvcPluginMasterApp.PluginsBase
@{
    var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
}
 
@foreach (var plugin in plugins)
{
    var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
    <li>
        <a href="@menuItem.Url">@menuItem.Name</a>
    </li>
}
در اینجا تمام افزونه‌ها به کمک structuremap یافت شده و سپس آیتم‌های منوی آن‌ها به صورت خودکار دریافت و اضافه می‌شوند.
سپس به فایل Layout.cshtml_ پروژه‌ی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آن‌را در بین سایر آیتم‌های منوی اصلی اضافه می‌کنیم:
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li>
                @{ Html.RenderPartial("_PluginsMenu"); }
            </ul>
        </div>
    </div>
</div>
اکنون اگر پروژه را اجرا کنیم، یک چنین شکلی را خواهد داشت:



بنابراین به صورت خلاصه

1) هر افزونه، یک پروژه‌ی کامل ASP.NET MVC است که پوشه‌های ریشه‌ی اصلی آن حذف شده‌اند و اطلاعات آن توسط یک Area جدید تامین می‌شوند.
2) تنظیم فضای نام مسیریابی‌های تمام پروژه‌ها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، می‌توان فایل‌های bin هر افزونه را توسط رخداد post-build، به پوشه‌ی bin پروژه‌ی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی می‌کند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک می‌گیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه می‌کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
مطالب
React 16x - قسمت 15 - مسیریابی - بخش 1 - تعریف و تنظیم مسیریابی‌ها
React برخلاف Angular، دارای قابلیت‌های توکار مسیریابی نیست و تنها کاری را که انجام می‌دهد همان رندر View است که تا اینجا بررسی کردیم. به همین جهت در این قسمت ابتدا یک برنامه‌ی عمومی و ساده را با نصب کتابخانه‌ی ثالثی برای توضیح مفاهیم مسیریابی، شامل کار با پارامترهای مسیریابی، کوئری استرینگ‌ها، هدایت کاربران به صفحات دیگر، مدیریت صفحات یافت نشده و مسیریابی تو در تو، بررسی می‌کنیم. سپس به عنوان تمرین، همان برنامه‌ی طراحی گریدی را که تا قسمت 14 تکمیل کردیم، با معرفی مسیریابی بهبود خواهیم بخشید.


برپایی پیش‌نیازها

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

همچنین کتابخانه‌ی ثالث بسیار معروف react-router-dom را نیز نصب می‌کنیم:
> npm i react-router-dom --save
نگارش dom آن مخصوص کار با مرورگر است و نگارش native آن (react-router-native)، مخصوص React Native و تولید برنامه‌های موبایل می‌باشد که مبحث دیگری است.


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

پس از نصب کتابخانه‌ی react-router-dom، برای افزودن آن به برنامه و فعالسازی مسیریابی، به فایل index.js مراجعه کرده و import آن‌را به ابتدای فایل اضافه می‌کنیم:
import { BrowserRouter } from "react-router-dom";
سپس کامپوننت App را داخل BrowserRouter، محصور می‌کنیم:
ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);
کار BrowserRouter، محصور سازی مدیریت تاریخچه‌ی مرور صفحات در مرورگر و انتقال آن به درخت کامپوننت‌های React است. به این ترتیب در هر قسمتی از درخت کامپوننت‌های برنامه می‌توان از History object مرورگر استفاده کرد.


ثبت و معرفی مسیریابی‌ها

در ادامه باید مسیریابی‌های خود را ثبت کنیم؛ به این معنا که بر اساس URL خاصی، چه کامپوننتی باید رندر شود. به همین جهت پوشه‌ی جدید src\components را ایجاد کرده و کامپوننت src\components\navbar.jsx را که یک کامپوننت تابعی بدون حالت است، در آن تعریف می‌کنیم:
import React from "react";

const NavBar = () => {
  return (
    <nav className="navbar bg-dark navbar-dark navbar-expand-sm">
      <div className="navbar-nav">
        <a className="nav-item nav-link" href="/">
          Home
        </a>
        <a className="nav-item nav-link" href="/products">
          Products
        </a>
        <a className="nav-item nav-link" href="/posts/2018/06">
          Posts
        </a>
        <a className="nav-item nav-link" href="/admin">
          Admin
        </a>
      </div>
    </nav>
  );
};

export default NavBar;
کار آن نمایش منوی بالای صفحه است.


سپس به فایل app.js مراجعه کرده و ساختار آن‌را به صورت زیر، جهت درج این NavBar، ویرایش می‌کنیم تا سبب رندر و نمایش منوی راهبری در مرورگر شود:
import "./App.css";

import React, { Component } from "react";

import NavBar from "./components/navbar";

class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
      </div>
    );
  }
}

export default App;
در ادامه در کامپوننت App، یک container را اضافه می‌کنیم که قرار است در آن بر اساس URL رسیده، محتوای کامپوننت خاصی رندر شود. به همین جهت کامپوننت Route را در اینجا قرار می‌دهیم و در آن یک یا چند Route را ثبت می‌کنیم:
import "./App.css";

import React, { Component } from "react";
import { Route } from "react-router-dom";

import Dashboard from "./components/admin/dashboard";
import Home from "./components/home";
import NavBar from "./components/navbar";
import Posts from "./components/posts";
import Products from "./components/products";

class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Route path="/products" component={Products} />
          <Route path="/posts" component={Posts} />
          <Route path="/admin" component={Dashboard} />
          <Route path="/" component={Home} />
        </div>
      </div>
    );
  }
}

export default App;
Route نیز یک کامپوننت است؛ همانند تمام کامپوننت‌هایی که تاکنون تعریف کردیم و دارای چند ویژگی است که به صورت props به آن منتقل می‌شوند. برای نمونه خاصیت path آن به مسیر products/ در مرورگر اشاره می‌کند و سبب رندر کامپوننت جدید Products که در بالای این ماژول نیز import شده، می‌شود. در اینجا سه مسیریابی دیگر را نیز ثبت کرده‌ایم که کامپوننت‌های جدید متناظر با آن‌ها به صورت زیر تعریف می‌شوند:

کامپوننت جدید src\components\products.jsx جهت رندر لیست آرایه‌ی اشیاء product:
import React, { Component } from "react";

class Products extends Component {
  state = {
    products: [
      { id: 1, name: "Product 1" },
      { id: 2, name: "Product 2" },
      { id: 3, name: "Product 3" }
    ]
  };

  render() {
    return (
      <div>
        <h1>Products</h1>
        <ul>
          {this.state.products.map(product => (
            <li key={product.id}>
              <a href={`/products/${product.id}`}>{product.name}</a>
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

export default Products;

کامپوننت بدون حالت تابعی src\components\home.jsx با این محتوا:
import React from "react";

const Home = () => {
  return <h1>Home</h1>;
};

export default Home;

کامپوننت بدون حالت تابعی src\components\posts.jsx با این محتوا:
import React from "react";

const Posts = () => {
  return (
    <div>
      <h1>Posts</h1>
      Year: , Month:
    </div>
  );
};

export default Posts;

کامپوننت بدون حالت تابعی src\components\admin\dashboard.jsx در پوشه‌ی جدید admin با این محتوا:
import React from "react";

const Dashboard = ({ match }) => {
  return (
    <div>
      <h1>Admin Dashboard</h1>
    </div>
  );
};

export default Dashboard;
تا اینجا اگر برنامه را اجرا کنیم، در اولین بار نمایش آن، شاهد رندر کامپوننت Home خواهیم بود. اما اگر در همین حالت بر روی لیست products، در منوی بالای صفحه کلیک کنیم، هم کامپوننت products و هم کامپونت home، هر دو با هم رندر شده‌اند. یک چنین رفتاری را در سایر صفحات نیز می‌توان مشاهده کرد:



معرفی کامپوننت Switch

<div className="container">
  <Route path="/products" component={Products} />
  <Route path="/posts" component={Posts} />
  <Route path="/admin" component={Dashboard} />
  <Route path="/" component={Home} />
</div>
الگوریتم تطابق کامپوننت Route، ابتدا بررسی می‌کند که آیا برای مثال URL ای با path مساوی products/ شروع شده‌است؟ اگر اینطور است، کامپوننت متناظر با آن را که برای نمونه در اینجا Products است، رندر می‌کند. این حالت جهت مسیری مانند products/new/ نیز صدق می‌کند؛ چون این URL نیز با products/ شروع شده‌است. همچنین این تطابق‌گر، مسیر ثبت شده‌ی برای کامپوننت Home را نیز چون با / شروع شده‌است و جزء ابتدایی مسیر products/ است هم رندر می‌کند. به همین جهت است که وقتی مسیر products/ را درخواست می‌دهیم، در صفحه دو کامپوننت products و home، با هم رندر می‌شوند.
یک روش حل این مشکل، استفاده از ویژگی exact است:
<Route path="/" exact component={Home} />
به این ترتیب اگر مسیر درخواستی دقیقا مساوی / بود، کامپوننت Home را رندر خواهد کرد. با این تغییر، با مراجعه‌ی به آدرس products/، دیگر رندر کامپوننت home را شاهد نخواهیم بود:


راه دوم رفع این مشکل، استفاده از کامپوننت Switch است. به همین جهت ابتدا این کامپوننت را import می‌کنیم:
import { Route, Switch } from "react-router-dom";
سپس تمام Routeهای تعریف شده را داخل Switch محصور خواهیم کرد:
class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Switch>
            <Route path="/products" component={Products} />
            <Route path="/posts" component={Posts} />
            <Route path="/admin" component={Dashboard} />
            <Route path="/"  component={Home} />
          </Switch>
        </div>
      </div>
    );
  }
}
Switch اولین مسیریابی را که با URL داده شده تطابق داشته باشد، رندر می‌کند. همچنین در اینجا دیگر نیازی به ذکر ویژگی exact نیز وجود ندارد. بنابراین با استفاده از Switch اگر مسیر داده شده، products/ باشد، مسیریابی تعریف شده‌ی با آن یافت می‌شود که در اینجا اولین Route تعریف شده‌است. سپس کار رندر کامپوننت آن‌را انجام داده و از مابقی مسیریابی‌های تعریف شده، صرفنظر می‌کند.
بنابراین هنگام کار با Switch، ترتیب مسیریابی‌های تعریف شده مهم است و باید از یک مسیریابی ویژه شروع شده و به یک مسیریابی عمومی مانند / ختم شود.


معرفی کامپوننت Link

تا اینجا اگر برنامه را اجرا کرده باشید و پیشتر سابقه‌ی کار با برنامه‌های SPA یا Single page applications را داشته باشید، یک مشکل دیگر را نیز احساس کرده‌اید: سیستم مسیریابی که تا کنون تعریف کرده‌ایم، به صورت SPA عمل نمی‌کند. یعنی به ازای هربار کلیک بر روی لینک‌های منوی راهبری سایت، یکبار دیگر به طور کامل برنامه از صفر بارگذاری مجدد می‌شود و تمام اسکریپت‌های آن مجددا از سرور دریافت شده و رندر خواهند شد. این مورد را در برگه‌ی network ابزارهای توسعه دهندگان مرورگر خود بهتر می‌توانید مشاهده کنید. به ازای هر درخواست نمایش کامپوننتی، تعدادی درخواست HTTP به سمت سرور ارسال می‌شوند که برای دریافت صفحه‌ی index و bundle.js برنامه هستند. اما در برنامه‌های SPA، مانند جمیل، با هربار کلیک بر روی لینکی، شاهد ریفرش و بارگذاری مجدد کل آن صفحه نیستیم و تنها اطلاعات موجود در قسمت container به روز می‌شوند.

یک نکته: در اینجا ممکن است دو درخواست websocket و info را نیز مشاهده کنید. این دو مرتبط به hot module reloading هستند که با ذخیره‌ی برنامه در ادیتور VSCode، بلافاصله سبب به روز رسانی و ریفرش برنامه در مرورگر می‌شوند.

برای رفع مشکل SPA نبودن برنامه، باید به کامپوننت NavBar مراجعه کرده و تمام anchor‌های استاندارد تعریف شده‌ی در آن‌را با کامپوننت Link جایگزین کنیم:
import React from "react";
import { Link } from "react-router-dom";

const NavBar = () => {
  return (
    <nav className="navbar bg-dark navbar-dark navbar-expand-sm">
      <div className="navbar-nav">
        <Link className="nav-item nav-link" to="/">
          Home
        </Link>
        <Link className="nav-item nav-link" to="/products">
          Products
        </Link>
        <Link className="nav-item nav-link" to="/posts/2018/06">
          Posts
        </Link>
        <Link className="nav-item nav-link" to="/admin">
          Admin
        </Link>
      </div>
    </nav>
  );
};

export default NavBar;
در اینجا ابتدا کامپوننت Link را در ابتدای ماژول، import کردیم. سپس تمام anchorها را یافته و تبدیل به کامپوننت Link نمودیم. همچنین href آن‌ها را نیز به ویژگی to تغییر دادیم.
با این تغییرات اگر برنامه را اجرا کنیم، اینبار با کلیک بر روی هر لینک، دیگر شاهد بارگذاری کامل صفحه در مرورگر نخواهیم بود؛ بلکه تنها قسمت container ای که کامپوننت Route مسیریابی در آن درج شده‌است، به روز رسانی می‌شود و این عملیات نیز بسیار سریع است؛ از این جهت که محتوای این کامپوننت‌ها از همان bundle.js حاوی تمام کدهای برنامه تامین می‌شود و این فایل تنها یکبار در آغاز برنامه از سرور خوانده شده و سپس توسط مرورگر پردازش می‌شود. بنابراین در برنامه‌های SPA، برخلاف برنامه‌های وب معمولی، هربار که کاربر آدرس متفاوتی را انتخاب می‌کند، بارگذاری مجدد برنامه و خوانده شدن محتوای متناظر از سرور صورت نمی‌گیرد؛ این محتوا هم اکنون در bundle.js برنامه مهیا است و قابلیت استفاده‌ی آنی را دارد.

اما کامپوننت Link چگونه کار می‌کند؟
کامپوننت لینک در نهایت همان anchor‌های استاندارد را رندر می‌کند؛ اما به هر کدام یک onClick را نیز اضافه می‌کند که سبب جلوگیری از رفتار پیش‌فرض anchor می‌شود. به همین جهت مرورگر درخواست اضافه‌ای را به سمت سرور ارسال نمی‌کند. در اینجا مدیریت کننده‌ی onClick، تنها Url بالای صفحه را در مرورگر تغییر می‌دهد. اکنون که Url تغییر کرده‌است، یکی از مسیریابی‌های تعریف شده، با این Url تطابق یافته و سپس کامپوننت متناظر با آن‌را رندر می‌کند.


بررسی Route props


اگر بر روی لینک نمایش products در منوی راهبری سایت کلیک کرده و سپس به خروجی افزونه‌ی react developer tools دقت کنیم (تصویر فوق)، مشاهده می‌کنیم که این کامپوننت هم اکنون تعدادی خاصیت را به صورت props در اختیار دارد؛ مانند history (امکان هدایت کاربر را به صفحه‌ای دیگر دارد)، location (آدرس جاری برنامه) و match (اطلاعاتی در مورد الگوریتم تطابق مسیر). کار تنظیم این props، توسط کامپوننت Route ای که کار ثبت مسیریابی‌ها را انجام می‌دهد، صورت می‌گیرد. به عبارتی کامپوننت Route، محصور کننده‌ی کامپوننتی است که آن‌را به عنوان پارامتر، دریافت و در صورت تطابق با مسیر جاری، آن‌را رندر می‌کند. همچنین در این بین کار تزریق خواص props یاد شده را نیز انجام می‌دهد.


ارسال props سفارشی در حین مسیریابی به کامپوننت‌ها

همانطور که بررسی کردیم، کامپوننت Route، حداقل سه خاصیت props را به کامپوننت‌هایی که رندر می‌کند، تزریق خواهد کرد. اما در اینجا برای تزریق خواص سفارشی چگونه باید عمل کرد؟
در حین کار با کامپوننت Route، برای ارسال props اضافی، بجای استفاده از ویژگی component آن، باید از ویژگی render استفاده کرد:
<Route
  path="/products"
  render={() => <Products param1="123" param2="456" />}
/>
در اینجا کار با تعریف یک arrow function شروع می‌شود که در نهایت المان کامپوننت مدنظر را همانند روش متداولی که برای تعریف تمام کامپوننت‌های React و تنظیم ویژگی‌های آن‌ها استفاده می‌شود، بازگشت می‌دهد که تاثیر آن‌را در خروجی افزونه‌ی react developer tools بهتر می‌توان مشاهده کرد:


البته اگر به تصویر فوق دقت کنید، سایر خواص پیشینی که تزریق شده بودند مانند history، location و match، دیگر در اینجا حضور ندارند. برای رفع این مشکل باید تعریف arrow function انجام شده را به صورت زیر تغییر داد:
<Route
  path="/products"
  render={props => (
    <Products param1="123" param2="456" {...props} />
  )}
/>
ابتدا پارامتر arrow function را به همان props تنظیم می‌کنیم. سپس با استفاده از spread operator، این props را در المان JSX تعریف شده، گسترده و تزریق می‌کنیم؛ با این خروجی:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-15-part-01.zip
مطالب
پیاده سازی Conventional UI در ASP.NET MVC
بعد از مدتی کار کردن با فریمورک ASP.NET MVC، شاید ایجاد یک فریمورک شخصی برپایه آن، یکی از باید‌ها برای شما باشد. در این راستا، نظم بخشیدن به ویوها برای جلوگیری از تکرار یکسری کد که اکثرا مورد استفاده قرار میگیرند، نجات بخش خواهد بود.
به تصویر زیر که حاصل از ویو مربوط به ویرایش یک Issue است، توجه فرمایید:

آیا به این نتیجه رسیدید که اصل DRY  را نقض کرده‌ایم؟ بله همین طور است. تکرار کلاس‌های css مربوط به بوت استرپ، تکرار هلپرهای توکار ASP.NET MVC بارها و بارها، خوانایی کد را پایین میارود و در برخی موارد هم خسته کننده خواهد بود. اگر با مباحث مربوط به EditorTemplate‌ها قبلا آشنا شده باشید، خیلی سریع عنوان خواهید کرد که بهتر است از این امکان بهره برد؛ بله درست است. برای این منظور در مسیر Views/Shared/EditorTemplates، فایل cshtml. همنام با نوع داده مد نظر را ایجاد میکنیم.

String.cshtml

@model string
    @Html.TextBox("",ViewData.TemplateInfo.FormattedModelValue,
    new { @class="form-control",placeholder=ViewData.ModelMetadata.Watermark})

Enum.cshtml

@model Enum
@Html.EnumDropDownListFor(m => Model, new { @class = "form-control" })

حال دوباره به نتیجه حاصل از تغییرات اعمال شده توجه کنید:

این نتیجه امیدوار کننده است ولی  بازهم یکسری از کدها بی دلیل تکرار شده‌اند. هلپرهای زیر نیز میتوانند در کاهش کدها به کمک ما برسند :

public static class BootstrapHelpers
    {
        public static IHtmlString BootstrapLabelFor<TModel,TProp>(
            this HtmlHelper<TModel> helper,
            Expression<Func<TModel,TProp>> property)
        {
            return helper.LabelFor(property, new
            {
                @class = "col-md-2 control-label"
            });
        }
        public static IHtmlString BootstrapLabel(
            this HtmlHelper helper,
            string propertyName)
        {
            return helper.Label(propertyName, new
            {
                @class = "col-md-2 control-label"
            });
        }
    }

از کلاس بالا برای عدم تکرار کلاس‌های بوت استرپ مربوط به Label، استفاده میشود .

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

خیلی عالی؛ توانستیم از تکرار یکسری از کلاس‌های بوت استرپ خلاص شویم. اما در ادامه با استفاده از یک Object Template به عنوان EditorTemplate برای نوع داده‌های Complex، کار را تمام خواهیم کرد.

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

Object.cshtml

@model dynamic

@foreach (var prop in ViewData.ModelMetadata.Properties
                            .Where(p => p.ShowForEdit))
{
    if (prop.TemplateHint == "HiddenInput")
    {
        @Html.Hidden(prop.PropertyName)
    }
    else
    {
        <div class="form-group">
            @Html.BootstrapLabel(prop.PropertyName)
            <div class="col-md-10">
                @Html.Editor(prop.PropertyName)
                @Html.ValidationMessage(prop.PropertyName)
            </div>
        </div>
    }
}

با استفاده از ViewData.ModelMetadata میتوان به خصوصیات مدل مربوط به ویو دسترسی پیدا کرد که در بالا با استفاده از همین خصوصیت به تمام پراپرتی‌های مدل دسترسی پیدا کرده و مقداری کد تکراری باقی مانده را هم در اینجا کپسوله کردیم.

حال کافی است به شکل زیر عمل کنیم:

در ادامه میتوان با پیاده سازی یک ModelMetadataProvider سفارشی برای اعمال قرارد‌ادهای مورد نیاز، نیز استفاده کرد و همچنین با سفارشی سازی فایل‌های T4 مرتبط با ویوهای تولیدی، به نتایح خیلی بهتری هم دست یافت.
مطالب
آشنایی با آزمایش واحد (unit testing) در دات نت، قسمت 4

ادامه آشنایی با NUnit

اگر قسمت سوم را دنبال کرده باشید احتمالا از تعداد مراحلی که باید در خارج از IDE صورت گیرد گلایه خواهید کرد (کامپایل کن، اجرا کن، اتچ کن، باز کن، ذخیره کن، اجرا کن و ...). خوشبختانه افزونه ReSharper این مراحل را بسیار ساده و مجتمع کرده است.
این افزونه به صورت خودکار متدهای آزمایش واحد یک پروژه را تشخیص داده و آنها را با آیکون‌هایی ( Gutter icons ) متمایز مشخص می‌سازد (شکل زیر). پس از کلیک بر روی آن‌ها ، امکان اجرای آزمایش یا حتی دیباگ کردن سطر به سطر یک متد آزمایش واحد درون IDE‌ ویژوال استودیو وجود خواهد داشت.



برای نمونه پس از اجرای آزمایش واحد قسمت قبل، نتیجه حاصل مانند شکل زیر خواهد بود:



راه دیگر، استفاده از افزونه TestDriven.NET است که نحوه استفاده از آن‌را اینجا می‌توانید ملاحظه نمائید. به منوی جهنده کلیک راست بر روی یک صفحه، گزینه run tests را اضافه می‌کند و نتیجه حاصل را در پنجره output ویژوال استودیو نمایش می‌دهد.

ساختار کلی یک کلاس آزمایش واحد مبتنی بر NUnit framework :

using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;

namespace TestLibrary
{
[TestFixture]
public class Test2
{
[SetUp]
public void MyInit()
{
//کدی که در این قسمت قرار می‌گیرد پیش از اجرای هر متد تستی اجرا خواهد شد
}

[TearDown]
public void MyClean()
{
//کدی که در این قسمت قرار می‌گیرد پس از اجرای هر متد تستی اجرا خواهد شد
}

[TestFixtureSetUp]
public void MyTestFixtureSetUp()
{
// کدی که در اینجا قرار می‌گیرد در ابتدای بررسی آزمایش واحد و فقط یکبار اجرا می‌شود
}

[TestFixtureTearDown]
public void MyTestFixtureTearDown()
{
// کدهای این قسمت در پایان کار یک کلاس آزمایش واحد اجرا خواهند شد
}

[Test]
public void Test1()
{
//بدنه آزمایش واحد در اینجا قرار می‌گیرد
Assert.That(2, Is.EqualTo(2));
}
}
}

شبیه به روال‌های رخداد گردان load و close یک فرم، یک کلاس آزمایش واحد NUnit نیز دارای ویژگی‌های TestFixtureSetUp و TestFixtureTearDown است که در ابتدا و انتهای آزمایش واحد اجرا خواهند شد (برای درک بهتر موضوع و دنبال کردن نحوه‌ی اجرای این روال‌ها، داخل این توابع break point قرار دهید و با استفاده از ReSharper ، آزمایش را در حالت دیباگ آغاز کنید)، یا SetUp و TearDown که در زمان آغاز و پایان بررسی هر متد آزمایش واحدی فراخوانی می‌شوند.
همانطور که در قسمت قبل نیز ذکر شد، به امضاهای متدها و کلاس فوق دقت نمائید (عمومی ، void و بدون آرگومان ورودی).
بهتر است از ویژگی‌های SetUp و TearDown با دقت استفاده نمود. عموما هدف از این روال‌ها ایجاد یک شیء و تخریب و پاک سازی آن است. حال اینکه این روال‌ها قبل و پس از اجرای هر متد آزمایش واحدی فراخوانی می‌شوند. بنابراین به این موضوع دقت داشته باشید.
همچنین توصیه می‌شود که کلاس‌های آزمایش واحد را در اسمبلی دیگری مجزا از پروژه اصلی پیاده سازی کنید (برای مثال یک پروژه جدید از نوع class library)، زیرا این موارد مرتبط با بررسی کیفیت کدهای شما هستند که موضوع جداگانه‌ای نسبت به پروژه اصلی محسوب می‌گردد (نحوه پیاده سازی آن‌‌را در قسمت قبل ملاحظه نمودید). همچنین در یک پروژه تیمی این جدا سازی، مدیریت آزمایشات را ساده‌تر می‌سازد و بعلاوه سبب حجیم شدن بی‌مورد اسمبلی‌های اصلی محصول شما نیز نمی‌گردند.


ادامه دارد...

مطالب
کمی درباره httpmodule
قبل از اینکه به httpmodule‌ها بپردازیم، اجازه بدید کمی در در مورد  httphandler  اطلاعات کسب کنیم. httphandler ویژگی است که از asp.net  به بعد ایجاد شد و در asp کلاسیک خبری از آن نیست.
یک httphandler کامپوننتی است که از طریق اینترفیس System.Web.IHttpHandler پیاده سازی میشود و به پردازش درخواست‌های رسیده از httprequest رسیدگی می‌کند.
فرض کنید کاربری درخواست صفحه default.aspx را کرده است و سرور هم پاسخ آن را می‌دهد. در واقع پردازش اینکه چه پاسخی باید به کاربر یا کلاینت ارسال شود بر عهده این کامپوننت می‌باشد. برای وب سرویس هم موضوع به همین صورت است؛ هر نوع درخواست HTTP از این طریق انجام می‌شود.
حال به سراغ httpmodule می‌رویم. httpmodule‌ها اسمبلی یا ماژول‌هایی هستن که بر سر راه هر درخواست کاربر از سرور قرار گرفته و قبل از اینکه درخواست شما به httphandler برسد، اول از فیلتر این‌ها رد می‌شود. در واقع موقعی که شما درخواست صفحه default.aspx را می‌کنید، درخواست شما به موتور asp.net ارسال می‌شود و از میان فیلترهایی رد می‌شود تا به دست httphandler برای پردازش خروجی برسد. برای همین اگر گاهی به جای گفتن asp.net engine عبارت asp.net pipeline هم میگویند همین هست؛ چون درخواست شما از بین بخش‌های زیادی می‌گذرد تا به httphandler برسد که httpmodule یکی از آن بخش هاست. با هر درخواستی که سرور ارسال می‌شود، httpmodule‌ها صدا زده می‌شوند و به برنامه نویس امکان بررسی اطلاعات درخواستی و پردازش درخواست‌ها را در ورودی و خروجی، می‌دهد و شما میتوانید هر عملی را که نیاز دارید انجام دهید. تعدادی از این ماژول‌های آماده، همان state‌ها و Authentication می‌باشند.
تصویر زیر نحوه‌ی ارسال و بازگشت یک درخواست را به سمت httphandler نشان می‌دهد


برنامه نویس هم میتواند با استفاده از اینترفیس‌های IHttpModule و IHttpHandler در درخواست‌ها دخالت نماید.
برای شروع یک کلاس ایجاد کنید که اینترفیس IHttpModule را پیاده سازی می‌کند. شما دو متد را باید در این کلاس بنویسید؛ یکی Init و دیگر Dispose. همانطور که مطلع هستید، اولی در ابتدای ایجاد شیء و دیگر موقع از دست رفتن شی صدا زده می‌شود.
متد Init یک آرگومان از نوع httpapplication دارد که مانند رسم نامگذاری متغیرها، بیشتر به اسم context یا app نام گذاری می‌شوند:
public void Init(HttpApplication app)
{
   app.BeginRequest += new EventHandler(OnBeginRequest);
}

public void Dispose(){ }
همانطور که می‌بینید این شیء یک رویداد دارد که ما این رویداد را به تابعی به نام OnBeginRequest متصل کردیم. سایر رویداد‌های موجود در httpapplication  به شرح زیر می‌باشند:
 BeginRequest  این رویداد اولین رویدادی است که اجرا می‌شود، هر نوع عملی که میخواهید در ابتدای ارسال درخواست انجام دهید، باید در این قسمت قرار بگیرد؛ مثلا قرار دادن یک بنر بالای صفحه 
AuthenticateRequest خود دانت از یک سیستم امنیتی توکار بهره مند است و اگر می‌خواهید در مورد آن خصوصی سازی انجام بدهید، این رویداد می‌تواند کمکتان کند 
AuthorizeRequest بعد از رویداد بالا، این رویداد برای شناسایی انجام می‌شود. مثلا دسترسی ها؛ دسترسی به قسمت هایی خاصی از منابع به او داده شود و قسمت هایی بعضی از منابع از او گرفته شود.
ResolveRequestCache این رویداد برای کش کردن اطلاعات استفاده می‌شود. خود دانت تمامی این رویدادها را به صورت تو کار فراهم آورده است؛ ولی اگر باز خصوصی سازی خاصی مد نظر شماست می‌توانید در این قسمت‌ها، تغییراتی را اعمال کنید. مثلا ایجاد file caching به جای memory cache و ... 
AcquireRequestState این قسمت برای مدیریت state می‌باشد مثلا مدیریت session ها
PreRequestHandlerExecute این رویداد قبل از httphandler اجرا می‌شود.
PostRequestHandlerExecute این رویداد بعد از httphandler اجرا می‌شود.
ReleaseRequestState این رویداد برای این صدا زده می‌شود که به شما بگوید عملیات درخواست پایان یافته است و باید state‌های ایجاد شده را release یا رها کنید.
UpdateRequestCache   برای خصوصی سازی output cache بکار می‌رود.
EndRequest عملیات درخواست پایان یافته است. در صورتیکه قصد نوشتن دیباگری در طی تمامی عملیات دارید، میتواند به شما کمک کند. 
PreSendRequestHeaders این رویداد قبل از ارسال طلاعات هدر هست. اگر قصد اضافه کردن اطلاعاتی به هدر دارید، این رویداد را به کار ببرید.
PreSendRequestContent  این رویداد موقعی صدا زده می‌شود که متد response.flush فراخوانی شود.، اگر می‌خواهید به محتوا چیزی اضافه کنید، از اینجا کمک بگیرید.
Error این رویداد موقعی رخ می‌دهد که یک استثنای مدیریت نشده رخ بدهد. برای نوشتن سیستم خطایابی خصوصی از این قسمت عمل کنید.
Disposed  این رویداد موقعی صدا زده میشود که درخواست، بنا به هر دلیلی پایان یافته است. برای عملیات پاکسازی و .. می‌شود از آن استفاده کرد. مثلا یک جور rollback برای کارهای انجام گرفته.

کد زیر را در نظر بگیرید:
کد زیر یک رویداد را تعریف کرده و سپس خود httpapplication را به عنوان sender استفاده می‌کند.
در اینجا قصد داریم یکی از صفحات را در خروجی تغییر دهیم. آدرس تایپ شده همان باشد ولی صفحه‌ی درخواست شده، صفحه‌ی دیگری است. این کار موقعی بیشتر کاربردی است که آدرس یک صفحه تغییر کرده و کاربر آدرس قبلی را وارد می‌کند. حالا یا از طریق بوک مارک یا از طریق یک لینک، در یک جای دیگر و شما میخواهید او را به صفحه‌ای جدید انتقال دهید، ولی در نوار آدرس، همان آدرس قبلی باقی بماند. همچنین کار دیگری که قرار است انجام بگیرد محاسبه مدت زمان رسیدگی به درخواست را محاسبه کند ، برای همین در رویداد BeginRequest زمان شروع درخواست را ذخیره و در رویداد EndRequest با به دست آوردن اختلاف زمان فعلی با زمان شروع به مدت زمان مربوطه پی خواهیم برد.
با استفاده از app.Context.Request.RawUrl آدرس اصلی و درخواست شده را یافته و در صورتی که شامل نام صفحه مربوطه بود، با نام صفحه‌ی جدید جابجا می‌کنیم تا اطلاعات به صفحه‌ی جدید پاس شوند ولی در نوار آدرس، هنوز آدرس قبلی یا درخواست شده، قابل مشاهده است.
در خط ["app.Context.Items["start  که یک کلاس ارث بری شده از اینترفیس IDictionary است، بر اساس کلید، داده شما را ذخیره و در مواقع لزوم در هر رویداد به شما باز می‌گرداند.
 public class UrlPath : IHttpModule
    {
        public void Init(HttpApplication app)
        {
            app.BeginRequest+=new EventHandler(_BeginRequest);
            app.EndRequest+=new EventHandler(_EndRequest);
        }

        public void Dispose()
        {

        }

         void _BeginRequest(object sender, EventArgs e)
         {
             
             HttpApplication app = (HttpApplication) sender;
             app.Context.Items["start"] = DateTime.Now;

             if (app.Context.Request.RawUrl.ToLower().Contains("tours_list.aspx"))
             {
                 app.Context.RewritePath(app.Context.Request.RawUrl.ToLower().Replace("tours_list.aspx","tours_cat.aspx"));
             }

         }
         void _EndRequest(object sender, EventArgs e)
         {
             HttpApplication app = (HttpApplication)sender;
             string log = (DateTime.Now - DateTime.Parse(app.Context.Items["start"].ToString())).ToString();
             Debugger.Log(0,"duration","request took " + log+Environment.NewLine); 
             
         } 
    }
حالا باید کلاس نوشته شده را به عنوان یک httpmodule به سیستم معرفی کنیم. به همین منظور وارد web.config شوید و کلاس جدید را معرفی کنید:
  <httpModules>
     <add name="UrlPath" type="UrlPath"/> 
   </httpModules>
اگر کلاس شما داخل یک namespace قرار دارد، در قسمت type حتما قبل از نام کلاس، آن را تعریف کنید namspace.ClassName
حالا دیگر کلاس UrlPath  به عنوان یک httpmodule به سیستم معرفی شده است. تگ httpmodule را بین تگ <system.web> قرار داده ایم.
در ادامه پروژه را start بزنید تا نتیجه کار را ببینید:
اگر IIS شما، هم نسخه‌ی IIS من باشد، نباید تفاوتی مشاهده کنید و می‌بینید که درخواست‌ها هیچ تغییری نکردند؛ چرا که اصلا httpmodule اجرا نشده است. در واقع در نسخه‌های قدیمی IIS یعنی 6 به قبل، این تعریف درست است ولی از نسخه‌ی 7 به بعد IIS، روش دیگری برای تعریف را قبول دارد و باید تگ httpmodule، بین دو تگ <syste.webserver> قرار بگیرد و نام تگ httpmodule به module تغییر پیدا کند.
پس کد فوق به این صورت تغییر می‌کند:
<system.webServer>
  
    <modules>
      <add name="UrlRewrite" type="UrlRewrite"/>
    </modules>
</system.webServer>
حالا اگر قصدا دارید که پروژه‌ی شما در هر دو IIS مورد حمایت قرار گیرد، باید این ماژول را در هر دو جا معرفی کرده و در تگ system.webserver  نیاز است تگ زیر تعریف شود که به طوری پیش فرض در webconfig می‌باشد:
    <validation validateIntegratedModeConfiguration="false"/>
در غیر این صورت خطای زیر را دریافت می‌کنید:

HTTP Error 500.22 - Internal Server Error 

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

Global.asax و HttpModule
اگر با global.asax کار کرده باشید حتما می‌پرسید که الان چه تفاوتی با httpmodule دارد؟ در فایل global هم همین‌ها را دارید و دقیقا همین مزایا مهیاست؛ در واقع global.asax یک پیاده سازی از httpapplication هست.
کلاس‌های httpmodule  نام دیگری هم دارند به اسم Portable global .asax به معنی یک فایل global.asax قابل حمل یا پرتابل. دلیل این نام گذاری این هست که شما موقعی که یک کد را در فایل global می‌نویسید، برای همیشه آن کد متعلق به همان پروژه هست و قابل انتقال به یک پروژه دیگر نیست ولی شما میتوانید httpmodule‌ها را در قالب یک پروژه به هر پروژه ای که دوست دارید رفرنس کنید و کد شما قابلیت استفاده مجدد و Reuse پیدا می‌کند و هم اینکه در صورت نیاز می‌توانید آن‌ها را در قالب یک dll منتشر کنید.
مطالب
بومی سازی تاریخ و اعداد در جاوا اسکریپت در سال 2020
احتمالا تا به امروز در برنامه‌های جاوا اسکریپتی خود از کتابخانه‌های ثالث و یا توابع ویژه‌ای برای نمایش شمسی تاریخ، نمایش فارسی اعداد، افزودن جدا کننده‌ی سه رقمی اعداد (جداکننده‌ی هزارگان)، نمایش تاریخ نسبی مانند 1 روز قبل و ... استفاده کرده‌اید. خبر خوب اینکه موتور جاوا اسکریپتی تمام مرورگرهای جدید (از فایرفاکس 29 و کروم 24 به بعد) به صورت توکار یک چنین تبدیل‌هایی را پشتیبانی می‌کنند و برای مثال برای تبدیل تاریخ میلادی به شمسی و نمایش آن، در بسیاری از موارد نیازی به کتابخانه‌ی حجیم moment.js (و یا سایر روش‌های مرسوم خانگی) نیست.


معرفی API استاندارد بومی سازی JavaScript

Internationalization یا به صورت خلاصه i18n (یعنی یک i به همراه 18 حرف و یک n)، پروسه‌ای که در آن برنامه به نحوی طراحی می‌شود تا خروجی آن قابلیت استفاده‌ی برای انواع و اقسام فرهنگ‌ها را داشته باشد. برای مثال دو متد زیر را در نظر بگیرید:
function formatDate(d)
{
  var month = d.getMonth() + 1;
  var date = d.getDate();
  var year = d.getFullYear();
  return month + "/" + date + "/" + year;
}

function formatMoney(amount)
{
  return "$" + amount.toFixed(2);
}
آیا در همه جای دنیا، تاریخ به صورت ماه، روز و سال نمایش داده می‌شود؛ آن هم به صورت میلادی؟ و یا آیا خروجی فرمت شده‌ی یک مقدار پولی، همیشه با دلار شروع می‌شود و نمایش آن نیز با اعداد انگلیسی است؟
پیشتر جاوا اسکریپت برای مدیریت یک چنین مواردی (i18n-aware formatting) از متد toLocaleString استفاده می‌کرد (و هنوز هم برای پشتیبانی از برنامه‌های قدیمی، از API عمومی آن حذف نشده‌است) و خروجی آن از هر مرورگر و پیاده سازی خاصی، به مرورگر دیگری می‌تواند متفاوت باشد؛ حتی اگر جزئیات دقیقی هم درخواست شود. برای رفع این مشکل، استاندارد ECMAScript Internationalization API ارائه شد تا قابلیت‌های توکار i18n جاوا اسکریپت را بهبود بخشیده و همچنین یک‌دست کند. توسط آن امکان انتخاب یک یا چند منطقه‌ی خاص و سپس فرمت کردن تاریخ، اعداد و یا حتی مرتب سازی واژه‌ها و عبارات با معرفی collations، میسر می‌باشد. در اینجا حتی امکان سفارشی سازی این فرمت‌ها نیز پیش‌بینی شده‌است.


معرفی اینترفیس Intl

i18n API در یک شیء سراسری به نام Intl قابل دسترسی است و تعدادی از سازنده‌های آن Intl.Collator ،Intl.DateTimeFormat و Intl.NumberFormat نام دارند؛ مانند:
const result =  new Intl.NumberFormat("fa").format(123456)
برای کار با این شیء، نیازی به import هیچ ماژول و یا کتابخانه‌ای نیست و جزئی از جاوا اسکریپت استاندارد می‌باشد. به همین جهت کار با آن حجمی را به برنامه‌ی شما اضافه نخواهد کرد.

تمام این سازنده‌ها می‌توانند با یک فرهنگ و یا آرایه‌ا‌‌ی از فرهنگ‌های مدنظر شروع شوند:
const portugueseTime =  new Intl.DateTimeFormat(["pt-BR", "pt-PT"], options);
در مثال اول فرهنگ فارسی و در مثال دوم فرهنگ پرتغالی که در برزیل و پرتغال مورد استفاده‌است، ذکر شده‌اند.
پارامتر اختیاری دوم آن‌ها نیز تنظیماتی است که جهت سفارشی سازی این بومی سازی می‌توان تعریف کرد.


نمایش شمسی تاریخ میلادی توسط i18n API

پس از معرفی i18n API، اکنون می‌خواهیم در طی مثال‌هایی، تمام کتابخانه‌های ثالث تبدیل تاریخ میلادی به شمسی را کنار گذاشته و با استفاده از جاوا اسکریپت استاندارد، این تبدیل را انجام دهیم. پارامتر دوم سازنده‌ی new Intl.DateTimeFormat که تنظیمات آن‌را مشخص می‌کند، می‌تواند به همراه ترکیبی از موارد زیر باشد که مقادیر مجاز برای آن‌ها را نیز مشاهده می‌کنید:
{
  weekday: 'narrow' | 'short' | 'long',
  era: 'narrow' | 'short' | 'long',
  year: 'numeric' | '2-digit',
  month: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long',
  day: 'numeric' | '2-digit',
  hour: 'numeric' | '2-digit',
  minute: 'numeric' | '2-digit',
  second: 'numeric' | '2-digit',
  timeZoneName: 'short' | 'long',
// Time zone to express it in
  timeZone: 'Asia/Shanghai',
  // Force 12-hour or 24-hour
  hour12: true | false,
// Rarely-used options
  hourCycle: 'h11' | 'h12' | 'h23' | 'h24',
  formatMatcher: 'basic' | 'best fit'
}
برای نمونه، ذکر Intl.DateTimeFormat بدون هیچ تنظیمی و فقط با تعیین فرهنگ فارسی:
var dateFormat = new Intl.DateTimeFormat("fa");
console.log(dateFormat.format(Date.now()));
خروجی «۱۳۹۸/۱۲/۱» را نمایش می‌دهد.


نمایش تاریخ شمسی با فرمت «۱۳۹۸ اسفند ۱, پنجشنبه»

برای تبدیل تاریخ میلادی به شمسی می‌توان از سازنده‌ی new Intl.DateTimeFormat با فرهنگ fa استفاده کرد. در اینجا ذکر مقدار long برای نام روز هفته، سبب درج نام روز می‌شود. نمایش سال به صورت عددی تنظیم شده‌است، ماه را به صورت بلند و نام کامل نمایش می‌دهد و مقدار روز را به صورت عددی درج می‌کند. این اعداد نیز فارسی هستند:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const faDate = new Intl.DateTimeFormat("fa", {
  weekday: "long",
  year: "numeric",
  month: "long",
  day: "numeric"
}).format(date);
console.log(faDate);
که برای نمونه سبب درج خروجی «۱۳۹۸ اسفند ۱, پنجشنبه» در کنسول توسعه دهندگان مرورگر خواهد شد.

اگر فقط نیاز به نمایش «۱ اسفند ۱۳۹۸» بود، می‌توان از تنظیمات زیر که در آن ماه، روز و سال ذکر شده‌اند و در آن، ماه به صورت کامل و بلند نمایش داده می‌شود، استفاده کرد:
const isoString = new Date().toISOString();
const date = new Date(isoString);
console.log(
  new Intl.DateTimeFormat("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  }).format(date)
);

یک نکته: همین خروجی را با متد قدیمی toLocaleDateString نیز می‌توان به دست آورد؛ اما روش توصیه شده برای برنامه‌های جدید، همان استفاده از new Intl است.
console.log(
  new Date().toLocaleDateString("fa", {
    month: "long",
    day: "numeric",
    year: "numeric"
  })
);

نمایش تاریخ شمسی با فرمت «۹۸/۱۲/۱،‏ ۶:۳۰»

برای اینکار پس از ذکر فرهنگ fa، تمام اجزای تاریخ را به صورت عددی مشخص می‌کنیم و سال را نیز دو رقمی نمایش خواهیم داد:
const date = new Date(Date.UTC(2020, 1, 20, 3, 0, 0, 200));
const fmt = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "numeric",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});
console.log(fmt.format(date));
در این حالت اگر نیاز بود حتما اعداد ماه و روز، دو رقمی باشند، می‌توان تنظیم 2-digit را صریحا ذکر کرد:
const faDateTime = new Intl.DateTimeFormat("fa", {
  year: "2-digit",
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short"
}).format;
const now = Date.now();
console.log(faDateTime(now));
با خروجی «۹۸/۱۲/۰۱،‏ ۱۲:۵۹ (‎+۳:۳۰ گرینویچ)»

و یا اگر «۱ اسفند ۱۳۹۸،‏ ۰۹:۲۹ (UTC)» مدنظر بود، می‌توان ماه را به long تنظیم کرد و مقدار timeZone را صریحا ذکر نمود (که البته ذکر تنظیمات timeZone اختیاری است):
const faTime = new Intl.DateTimeFormat("fa", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  timeZoneName: "short",
  timeZone: "UTC"
});
console.log(faTime.format(now));

نمایش تاریخ‌های نسبی مانند «1 روز بعد»
برای نمایش تاریخ‌های نسبی، می‌توان از شیء new Intl.RelativeTimeFormat استفاده کرد:
const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit", // other values: "lookup"
  numeric: "always", // other values: "auto"
  style: "long", // other values: "short" or "narrow"
});
console.log(rtf.format(-1, "day"));
console.log(rtf.format(1, "day"));
با خروجی‌های «۱ روز پیش» و «۱ روز بعد»


نمایش اعداد فارسی توسط i18n API

احتمالا برای تبدیل اعداد انگلیسی به فارسی و نمایش آن‌ها، متدهایی را برای replace حروف و اعداد طراحی کرده‌اید. به کمک شیء استاندارد Intl.NumberFormat دیگر نیازی به آن‌ها نخواهید داشت!
خروجی شیء Intl.NumberFormat به همراه ذکر فرهنگ فارسی و هیچ تنظیم اضافه‌تری
console.log(new Intl.NumberFormat("fa").format(123456));
به صورت «۱۲۳٬۴۵۶» است که هم اعداد آن فارسی شده‌اند و هم به همراه جداکننده‌ی هزارگان خودکار است.

اگر می‌خواهید این جداکننده‌ی هزارگان نمایش داده نشود، نیاز است تنظیمات آن‌را به همراه useGrouping: false، به صورت زیر ذکر کرد:
console.log(
   new Intl.NumberFormat("fa", { useGrouping: false }).format(123456)
);

این شیء یک مقدار غیرعددی را
console.log(new Intl.NumberFormat("fa").format("تست"));
به صورت «ناعدد» نمایش می‌دهد.

و یا برای نمایش واحد پولی، می‌توان حالت نمایش را به currency و نوع currency را به IRR که ریال است، تنظیم کرد:
const gasPrice = new Intl.NumberFormat("fa", {
  style: "currency",
  currency: "IRR",
  minimumFractionDigits: 3
});
console.log(gasPrice.format(5.2597));
با این خروجی: «‎ریال ۵٫۲۶۰» که در اینجا امکان تنظیم نمایش تعداد اعشار آن نیز میسر است.

برای نمایش درصد پس از اعداد می‌توان از تنظیم زیر استفاده کرد:
const faPercent = new Intl.NumberFormat("fa", {
  style: "percent",
  minimumFractionDigits: 2
}).format;
console.log(faPercent(0.438));
که خروجی «۴۳٫۸۰٪» را نمایش می‌دهد.

و یا برای نمایش ممیز به همراه تنظیم دقت آن داریم:
const persianDecimal = new Intl.NumberFormat("fa", {
  minimumIntegerDigits: 2,
  maximumFractionDigits: 2
});
console.log(persianDecimal.format(3.1416));
با این خروجی: «۰۳٫۱۴»
مطالب
آشنایی با XSLT
XSLT در واقع یک StyleSheet یا یک راهنما در مورد تبدیل فایل‌های xml به انواع و یا ساختارهای دیگری چون فایل‌های html، فایل‌های متنی و ... است که توسط کنسرسیوم وب ارائه شده‌است. این فایل حاوی یک سری دستورالعمل برای برنامه‌های پردازشگر است که به آن‌ها می‌گوید چگونه این فایل را تبدیل کنند.

اساس کار XSLT
در تصویر زیر، فایل xml به همراه xslt، به تجزیه کننده یا تحلیل کننده داده میشوند. در این قسمت هر دو فایل منبع تحلیل شده و از روی فایل xml، درختی در حافظه تهیه می‌شود و از فایل xslt یک سری قوانین استخراج می‌شوند. بعد این دو محتوای جدید تولید شده، در اختیار XSL Processor قرار گرفته و از روی آن‌ها به ساخت درخت نتیجه (نوع درخواستی) در حافظه می‌رسد که در نهایت آن را به نام یک فایل مستند میکند.

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

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

ساختار فایل xml:
<Albums>
  <Album>
    <name>Modern Talking</name>
    <cover>http://album.com/a.jpg</cover>
    <Genres>
      <Genre>POP</Genre>
      <Genre>Jazz</Genre>
      <Genre>Classic</Genre>
    </Genres>
    <Description>
      this is a marvelous Album
    </Description>
    <price>
      25.99$
    </price>
  </Album>
</Albums>

ساختار فایل Html
در ابتدا فایل، برای معرفی فایل و رعایت قرارداد، فضای نام مربوطه را یا به شکل زیر
<xsl:stylesheet version="1.0"xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
یا بدین شکل
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
می‌نویسیم و سپس کل کدهای html را با تگ زیر محصور می‌کنیم:
<xsl:template match="/">
<html>
....
</html>
</xsl:template>
هر آدرسی که در match بنویسید، آدرس‌های دستورات داخلی در ادامه‌ی آن خواهند بود.

نمایش فیلدها
سپس در آن، کد html مورد نظر را وارد می‌کنیم و در میان این کدها، تگ‌های xslt را وارد می‌نماییم. کد زیر بخشی از صفحه هست که برای نمایش آلبوم‌ها استفاده می‌کنیم:
<div>
  <img src="" alt="image" />
  <h3>
    <xsl:value-of select="albums/album/name" />
  </h3>
  <h4>Artist Name 1</h4>
  <h5>POP</h5>
  <h6>25/5/2015</h6>
  <h7>this is description</h7>
  <div>
    <a href="#">More</a>
  </div>
</div>
تگ‌های xsl نشان دهنده دستورالعمل‌های این قالب هستند. این دستور وظیفه انتخاب یک آیتم و نمایش آن را دارد. در قسمت select، آدرس نام آلبوم را به صورت یک مسیر، از تگ والد به سمت پایین وارد کرده‌ایم. این دستور العمل با اولین گره که به آن برسد، نمایش می‌یابد ولی اگر بخواهیم کدهای بالا، به تعداد هر آیتم (آلبوم) تکرار شود، از دستور العمل حلقه استفاده می‌کنیم:
<xsl:for-each select="albums/album">
  <div>
    <img src="" alt="image" />
    <h3>
      <xsl:value-of select="name" />
    </h3>
    <h4>Artist Name 1</h4>
    <h5>POP</h5>
    <h6>25/5/2015</h6>
    <h7>this is description</h7>
    <div>
      <a href="#">More</a>
    </div>
  </div>
</xsl:for-each>
خطوط بالا، مرتبا گره‌های album را در گره Albums، یافته و همه تگ‌های داخلش را تکرار کرده و نام هر آلبوم را نیز چاپ می‌کند. موقعی که از حلقه استفاده می‌کنیم و مسیر گره والد در آن مشخص شده است، نیازی نیست که دیگر برای دریافت نام آلبوم، کل مسیر را ذکر کنید.

ایجاد ارتباط میان دو فایل XML و XSLT
برای اجرا و تست آن باید از طریق یک ابزار که توانایی تحلیل این دستورات را دارد، استفاده کنید. یکی از همین ابزارها، مرورگر شماست. برای اینکه به مرورگر ارتباط فایل xml و xsl را بفهمانیم، تکه کد زیر را در فایل xml جهت لینک شدن می‌نویسیم و سپس فایل xml را در مرورگر اجرا می‌کنیم:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="index.xslt"?>
الان تصویر بدین شکل نمایش داده می‌شود:



ساخت المان یا تگ
در ادامه بقیه فیلدها را تکمیل میکنیم. فیلد بعدی تصویر است که تصویر دیگر مانند متن، بین تگ‌ها قرار نمی‌گیرد و باید داخل attribute تگ تصویر درج شود. برای نمایش تصویر می‌توانیم به دو شکل عمل کنیم. کد را به صورت زیر بنویسیم:
<img src="{cover}" width="200px" height="200px">
  <xsl:value-of select="cover" />
</img>
یا اینکه المان مورد نظر را توسط xsl ایجاد کنیم:
<xsl:element name="img">
  <xsl:attribute name="src">
    <xsl:value-of select="cover"/>
  </xsl:attribute>
  <xsl:attribute name="width">
    200px
  </xsl:attribute>
  <xsl:attribute name="height">
    200px
  </xsl:attribute>
  <xsl:value-of select="cover"/>
</xsl:element>
حال با یکی از کدهای بالا، مرورگر را جهت تست اجرا می‌کنیم:


دستور العمل‌های بیشتر (مرتب سازی)
دستورات xslt فقط به چند تگ بالا خلاصه نمی‌شود؛ بلکه دستورات شرطی ،تعریف متغیرها، توابع داخلی جهت کار با اعداد، رشته‌ها و ... هم در آن وجود دارند. یکی از همین دستورات جذاب، مرتب سازی است. دستورالعل sort به ما اجازه می‌دهد تا بر اساس یک فیلد، داده‌ها را مرتب کنیم. با افزودن کد زیر بعد از دستور حلقه، لیست را بر اساس نام آلبوم مرتب می‌کنیم:
<xsl:for-each select="Albums/Album">
<xsl:sort select="name"/>

دستورات شرطی
حال تصمیم می‌گیریم که آلبوم‌های با قیمت بالاتر از 10 دلار را 2 دلار تخفیف دهیم. برای اینکار نیاز است تا با تگ if آشنا شویم:
<xsl:variable name="numprice" select="number(substring(price,1,string-length(price)-1))" />

<xsl:if test="$numprice>=10">
  <h4 >
    <span style='text-decoration:line-through'>
      <xsl:value-of select="price" />
    </span>
    <b>
      <xsl:value-of select="$numprice -2" />$
    </b>
  </h4>
</xsl:if>
در اینجا کمی مثال را پیچیده‌تر و از چند عنصر جدید در آن استفاده کرده‌ایم. در خط اول یک متغیر با نام numprice تعریف کرده‌ایم. متغیرها بر دو نوعند: محلی یا عمومی. برای تعریف متغیر عمومی لازم است آن را در بالای سند، بعد از تگ template ایجاد کنید و متغیرهای محلی فقط در داخل همان تگی که تعریف کرده‌اید، اعتبار دارند. از آنجا که قیمت‌ها به صورت رشته‌ای هستند و در انتها هم حرف $ را دارند، بهتر است که قیمت را بدون $ به عدد تبدیل کنیم تا بتوانیم بعدا در شرط، به عنوان عدد، مقایسه و در صورت صحت شرط، دو عدد از آن کم کنیم. در اینجا ما از تابع substring برای جداسازی رشته و از تابع string-lentgh برای دریافت طول رشته و در نهایت از تابع number برای تبدیل رشته به عدد استفاده کردیم و آن‌را برای استفاده‌های بعدی، در متغیر ذخیره کردیم (برای آشنایی بیشتر با این توابع به این آدرس رجوع کنید). سپس تگ if را صدا زدیم و در صورتی که مقدار داخل متغیر (علامت متغیر، استفاده از عبارت $ قبل از نام آن است ) از 10 بیشتر یا برابر آن بود، دو واحد از آن کم می‌کنیم و روی قیمت قبلی خط می‌کشیم. نتیجه حاصله در تصویر زیر مشخص است:

در حال حاضر مشکلی که وجود دارد این است که ما برای قیمت‌های زیر 10 دلار هیچ شرطی نداریم و این دستور if کمی سطحی برخورد می‌کند و برای قیمت‌های زیر 10 دلار مجددا به یک if نیازمندیم. ولی دستور دیگری، مشابه دستور switch وجود دارد که استفاده از آن در شرایط دو شرط بالا مقرون به صرفه است. نام این دستور choose می‌باشد. خطوط بالا را به شکل زیر تغییر می‌دهیم:
<xsl:variable name="numprice" select="number(substring(price,1,string-length(price)-1))" />
<xsl:choose>
  <xsl:when test="$numprice>=10">
    <h4 >
      <span style='text-decoration:line-through;color:red'>
        <xsl:value-of select="price" />
      </span>
      <b>
        <xsl:value-of select="$numprice -2" />$
      </b>
    </h4>
  </xsl:when>

  <xsl:otherwise>
    <b>
      <xsl:value-of select="$numprice" />$
    </b>
  </xsl:otherwise>
</xsl:choose>
شما می‌توانید به تعداد زیادی از تگ when برای اعمال دیگر شرط‌ها همانند دستور switch ...case استفاده کنید. در اینجا اگر قیمت‌ها زیر 10 دلار باشند، تغییری در قیمت ایجاد نکرده و خودش را نشان می‌دهیم. ولی اگر از 10 دلار به بالا باشد، قیمت‌ها دو دلار تخفیف می‌خورند. شکل زیر نتیجه حاصل از اضافه شدن کد بالاست:


استفاده از template ها
برای خلاصه سازی کار و جمع و جور کردن کدها می‌توان از template‌ها استفاده کرد. شما هم در ابتدا، یک قالب یا template را برای کل سند ایجاد کردید. حالا سعی ما این است که اینبار، قالب‌های کوچکتر و جرئی‌تر و اختصاصی‌تر ساخته و آن‌ها را در قالب اصلی صدا بزنیم. برای ساخت قالب به ریشه xsl:stylesheet رفته و یک template جدید را به شکل زیر ایجاد میکنیم:
<xsl:template match="DateOfRelease">

  <xsl:variable name="date" select="."/>
  <xsl:variable name="day" select="substring($date,1,2)"/>
  <xsl:variable name="month" select="number(substring($date,4,2))"/>
  <xsl:variable name="year" select="substring($date,7,4)"/>
  Release Date:

  <xsl:choose>
    <xsl:when test="$month = 1">
      <xsl:value-of select="$day"/>,Jan/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 2">
      <xsl:value-of select="$day"/>,Feb/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 3">
      <xsl:value-of select="$day"/>,Mar/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 4">
      <xsl:value-of select="$day"/>,Apr/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 5">
      <xsl:value-of select="$day"/>,May/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 6">
      <xsl:value-of select="$day"/>,Jun/<xsl:value-of select="$year"/>
    </xsl:when>

    <xsl:when test="$month = 7">
      <xsl:value-of select="$day"/>,Jul/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 8">
      <xsl:value-of select="$day"/>,Aug/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 9">
      <xsl:value-of select="$day"/>,Sep/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 10">
      <xsl:value-of select="$day"/>,Oct/<xsl:value-of select="$year"/>
    </xsl:when>
    <xsl:when test="$month = 11">
      <xsl:value-of select="$day"/>,Nov/<xsl:value-of select="$year"/>
    </xsl:when>

    <xsl:otherwise>
      <xsl:value-of select="$day"/>,Dec/<xsl:value-of select="$year"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>
در این قالب ما روی فیلد تاریخ کار میکنیم و قصد داریم تاریخ را در قالب نوشتاری دیگری درج کنیم. عبارت نقطه (.) در select خط اول به معنای گره جاری است. بعد از ساخت قالب جدید آن را در محل مورد نظر در قالب اصلی یا ریشه صدا می‌زنیم:
<xsl:for-each select="Albums/Album">
  <xsl:sort select="name"/>
  <div>
    <img src="{cover}" width="200px" height="200px">
      <xsl:value-of select="cover" />
    </img>
    <h3>
      <xsl:value-of select="name" />
    </h3>
    <h4>POP</h4>

    <xsl:variable name="numprice" select="number(substring(price,1,string-length(price)-1))" />
    <xsl:choose>
      <xsl:when test="$numprice>=10">
        <h4 >
          <span style='text-decoration:line-through;color:red'>
            <xsl:value-of select="price" />
          </span>
          <b>
            <xsl:value-of select="$numprice -2" />$
          </b>
        </h4>
      </xsl:when>

      <xsl:otherwise>
        <h4 >
          <b>
            <xsl:value-of select="$numprice -2" />$
          </b>
        </h4>
      </xsl:otherwise>
    </xsl:choose>
    <h4>
      <!-- محل صدا زدن قالب-->
      <xsl:apply-templates select="DateOfRelease"/>
    </h4>
    <h4>
      <xsl:value-of select="Description"/>
    </h4>
    <div>
      <a href="#">More</a>
    </div>
  </div>
</xsl:for-each>


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

<xsl:for-each select="Albums/Album[contains(Genres/Genre, 'POP')]">
دستور بالا میگوید که حلقه را آلبوم‌ها حرکت بده، به شرطی که در مسیر Genres/Genre مقداری برابر POP بیابی. حالا اگر بخواهیم این شرط را معکوس کنیم میتوانیم عبارت را به صورت زیر درج کنیم:
<xsl:for-each select="Albums/Album[not(contains(Genres/Genre, 'POP'))]">
برای شکیل شدن کار، بهتر است که نام سبک‌ها را در کنار تصویر هم درج کنیم. به همین علت دستور حلقه را مورد استفاده قرار می‌دهیم:
<xsl:for-each select="Genres/Genre">
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
,
</xsl:if>
</xsl:for-each>
دستور بالا شامل دو تابع جدید هست که مقدار برگشتی آن‌ها اندیس گره فعلی و اندیس آخرین گره می‌باشد و در عبارت شرطی ما تعیین کرده‌ایم که بین هر سبکی که می‌نویسد، یک علامت جدا کننده , هم قرار گیرد؛ به جز آخرین گره که دیگر نیازی به علامت جدا کننده ندارد. تصویر زیر آلبوم هایی را نشان میدهد که در سبک پاپ قرار گرفته‌اند:


تبدیل xml و xsl در دات نت

string xmlfile = Application.StartupPath + "\\sampleXML.xml";

//بارگذاری فایل قوانین در حافظه
XslTransform xslt = new XslTransform();
xslt.Load("sample-stylesheet.xsl");

//XPath ایجاد یک سند جدید بر اساس استاندارد 
XPathDocument xpath = new XPathDocument(xmlfile);

//آماده سازی برای نوشتن فایل نهایی
XmlTextWriter xwriter = new XmlTextWriter(xmlfile + ".html", Encoding.UTF8);

//نوشتن فایل مقصد بر اساس قوانین مشخص شده
xslt.Transform(xpath, null, xwriter, null);
xwriter.Close();
در صورتی که فایل XSL نیازی به تگ‌هایی داشته باشد که در فایل xml نیست و خودتان قصد دارید که آن‌ها را از این طریق وارد کنید، می‌توانید خطوط زیر را به کد بالا اضافه کنید:
XsltArgumentList xslArgs = new XsltArgumentList();
xslArgs.AddParam("logo", "", logo);
xslArgs.AddParam("name", "", name); 
....
xslt.Transform(xpath, xslArgs, xwriter, null);