مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
ASP.NET Core Identity به همراه دو قابلیت جدید است که پیاده سازی سطوح دسترسی پویا را با سهولت بیشتری میسر می‌کند:
الف) Policies
ب) Role Claims


سیاست‌های دسترسی یا Policies در ASP.NET Core Identity

ASP.NET Core Identity هنوز هم از مفهوم Roles پشتیبانی می‌کند. برای مثال می‌توان مشخص کرد که اکشن متدی و یا تمام اکشن متدهای یک کنترلر تنها توسط کاربران دارای نقش Admin قابل دسترسی باشند. اما نقش‌ها نیز در این سیستم جدید تنها نوعی از سیاست‌های دسترسی هستند.
[Authorize(Roles = ConstantRoles.Admin)]
public class RolesManagerController : Controller
برای مثال در اینجا دسترسی به امکانات مدیریت نقش‌های سیستم، به نقش ثابت و از پیش تعیین شده‌ی Admin منحصر شده‌است و تمام کاربرانی که این نقش به آن‌ها انتساب داده شود، امکان استفاده‌ی از این قابلیت‌ها را خواهند یافت.
اما نقش‌های ثابت، بسیار محدود و غیر قابل انعطاف هستند. برای رفع این مشکل مفهوم جدیدی را به نام Policy اضافه کرده‌اند.
[Authorize(Policy="RequireAdministratorRole")]
public IActionResult Get()
{
   /* .. */
}
سیاست‌های دسترسی بر اساس Requirements و یا نیازهای سیستم تعیین می‌شوند و تعیین نقش‌ها، تنها یکی از قابلیت‌های آن‌ها هستند.
برای مثال اگر بخواهیم تک نقش Admin را به صورت یک سیاست دسترسی جدید تعریف کنیم، روش کار به صورت ذیل خواهد بود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Admin"));
    });
}
در تنظیمات متد AddAuthorization، یک سیاست دسترسی جدید تعریف شده‌است که جهت برآورده شدن نیازمندی‌های آن، کاربر سیستم باید دارای نقش Admin باشد که نمونه‌ای از نحوه‌ی استفاده‌ی از آن‌را با ذکر [Authorize(Policy="RequireAdministratorRole")] ملاحظه کردید.
و یا بجای اینکه چند نقش مجاز به دسترسی منبعی را با کاما از هم جدا کنیم:
 [Authorize(Roles = "Administrator, PowerUser, BackupAdministrator")]
می‌توان یک سیاست دسترسی جدید را به نحو ذیل تعریف کرد که شامل تمام نقش‌های مورد نیاز باشد و سپس بجای ذکر Roles، از نام این Policy جدید استفاده کرد:
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
به این صورت
[Authorize(Policy = "ElevatedRights")]
public IActionResult Shutdown()
{
   return View();
}

سیاست‌های دسترسی تنها به نقش‌ها محدود نیستند:
services.AddAuthorization(options =>
{
   options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
برای مثال می‌توان دسترسی به یک منبع را بر اساس User Claims یک کاربر به نحوی که ملاحظه می‌کنید، محدود کرد:
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
   return View();
}


سیاست‌های دسترسی پویا در ASP.NET Core Identity

مهم‌ترین مزیت کار با سیاست‌های دسترسی، امکان سفارشی سازی و تهیه‌ی نمونه‌های پویای آن‌ها هستند؛ موردی که با نقش‌های ثابت سیستم قابل پیاده سازی نبوده و در نگارش‌های قبلی، جهت پویا سازی آن، یکی از روش‌های بسیار متداول، تهیه‌ی فیلتر Authorize سفارشی سازی شده بود. اما در اینجا دیگر نیازی نیست تا فیلتر Authorize را سفارشی سازی کنیم. با پیاده سازی یک AuthorizationHandler جدید و معرفی آن به سیستم، پردازش سیاست‌های دسترسی پویای به منابع، فعال می‌شود.
پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class DynamicPermissionRequirement : IAuthorizationRequirement
{
}
ابتدا باید یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی مانند روشی که در پروژه‌ی DNT Identity بکار گرفته شده‌است، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
پس از اینکه نیازمندی DynamicPermissionRequirement را تعریف کردیم، در ادامه باید یک AuthorizationHandler استفاده کننده‌ی از آن را تعریف کنیم:
    public class DynamicPermissionsAuthorizationHandler : AuthorizationHandler<DynamicPermissionRequirement>
    {
        private readonly ISecurityTrimmingService _securityTrimmingService;

        public DynamicPermissionsAuthorizationHandler(ISecurityTrimmingService securityTrimmingService)
        {
            _securityTrimmingService = securityTrimmingService;
            _securityTrimmingService.CheckArgumentIsNull(nameof(_securityTrimmingService));
        }

        protected override Task HandleRequirementAsync(
             AuthorizationHandlerContext context,
             DynamicPermissionRequirement requirement)
        {
            var mvcContext = context.Resource as AuthorizationFilterContext;
            if (mvcContext == null)
            {
                return Task.CompletedTask;
            }

            var actionDescriptor = mvcContext.ActionDescriptor;
            var area = actionDescriptor.RouteValues["area"];
            var controller = actionDescriptor.RouteValues["controller"];
            var action = actionDescriptor.RouteValues["action"];

            if(_securityTrimmingService.CanCurrentUserAccess(area, controller, action))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }

            return Task.CompletedTask;
        }
    }
کار با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن، توسط ASP.NET Core Identity استفاده می‌شود. بنابراین در اینجا DynamicPermissionRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی، بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.

منطق سفارشی پیاده سازی شده نیز به این صورت است:
نام ناحیه، کنترلر و اکشن متد درخواستی کاربر از مسیریابی جاری استخراج می‌شوند. سپس توسط سرویس سفارشی ISecurityTrimmingService تهیه شده، بررسی می‌کنیم که آیا کاربر جاری به این سه مؤلفه دسترسی دارد یا خیر؟

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
        private static void addDynamicPermissionsPolicy(this IServiceCollection services)
        {
            services.AddScoped<IAuthorizationHandler, DynamicPermissionsAuthorizationHandler>();
            services.AddAuthorization(opts =>
            {
                opts.AddPolicy(
                    name: ConstantPolicies.DynamicPermission,
                    configurePolicy: policy =>
                    {
                        policy.RequireAuthenticatedUser();
                        policy.Requirements.Add(new DynamicPermissionRequirement());
                    });
            });
        }
ابتدا باید DynamicPermissionsAuthorizationHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه DynamicPermission تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از DynamicPermissionRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس DynamicPermissionRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها، بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy = ConstantPolicies.DynamicPermission)]
[DisplayName("کنترلر نمونه با سطح دسترسی پویا")]
public class DynamicPermissionsSampleController : Controller
به صورت خودکار توسط DynamicPermissionsAuthorizationHandler مدیریت می‌شود.


سرویس ISecurityTrimmingService چگونه کار می‌کند؟

کدهای کامل ISecurityTrimmingService را در کلاس SecurityTrimmingService می‌توانید مشاهده کنید.
پیشنیاز درک عملکرد آن، آشنایی با دو قابلیت زیر هستند:
الف) «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»
دقیقا از همین سرویس توسعه داده شده‌ی در مطلب فوق، در اینجا نیز استفاده شده‌است؛ با یک تفاوت تکمیلی:
public interface IMvcActionsDiscoveryService
{
    ICollection<MvcControllerViewModel> MvcControllers { get; }
    ICollection<MvcControllerViewModel> GetAllSecuredControllerActionsWithPolicy(string policyName);
}
از متد GetAllSecuredControllerActionsWithPolicy جهت یافتن تمام اکشن متدهایی که مزین به ویژگی Authorize هستند و دارای Policy مساوی DynamicPermission می‌باشند، در کنترلر DynamicRoleClaimsManagerController برای لیست کردن آن‌ها استفاده می‌شود. اگر این اکشن متد مزین به ویژگی DisplayName نیز بود (مانند مثال فوق و یا کنترلر نمونه DynamicPermissionsSampleController)، از مقدار آن برای نمایش نام این اکشن متد استفاده خواهد شد.
بنابراین همینقدر که تعریف ذیل یافت شود، این اکشن متد نیز در صفحه‌ی مدیریت سطوح دسترسی پویا لیست خواهد شد.
 [Authorize(Policy = ConstantPolicies.DynamicPermission)]

ابتدا به مدیریت نقش‌های ثابت سیستم می‌رسیم. سپس به هر نقش می‌توان یک ‍Claim جدید را با مقدار area:controller:action انتساب داد.
به این ترتیب می‌توان به یک نقش، تعدادی اکشن متد را نسبت داد و سطوح دسترسی به آن‌ها را پویا کرد. اما ذخیره سازی آن‌ها چگونه است و چگونه می‌توان به اطلاعات نهایی ذخیره شده دسترسی پیدا کرد؟


مفهوم جدید Role Claims در ASP.NET Core Identity

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



وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث می‌برد. هدف از نقش‌ها، گروه بندی کاربران است. توسط Role Claims می‌توان مشخص کرد این نقش‌ها چه کارهایی را می‌توانند انجام دهند. اگر از قسمت قبل بخاطر داشته باشید، سرویس توکار UserClaimsPrincipalFactory دارای مرحله‌ی 5 ذیل است:
«5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).»
به این معنا که با لاگین شخص به سیستم، تمام اطلاعات مرتبط به او که در جدول AppRoleClaims وجود دارند، به کوکی او به صورت خودکار اضافه خواهند شد و دسترسی به آن‌ها فوق العاده سریع است.

در کنترلر DynamicRoleClaimsManagerController، یک Role Claim Type جدید به نام DynamicPermissionClaimType اضافه شده‌است و سپس ID اکشن متدهای انتخابی را به نقش جاری، تحت Claim Type عنوان شده، اضافه می‌کند (تصویر فوق). این ID به صورت area:controller:action طراحی شده‌است. به همین جهت است که در  DynamicPermissionsAuthorizationHandler همین سه جزء از سیستم مسیریابی استخراج و در سرویس SecurityTrimmingService مورد بررسی قرار می‌گیرد:
 return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType &&
claim.Value == currentClaimValue);
در اینجا user همان کاربرجاری سیستم است. HasClaim جزو متدهای استاندارد آن است و Type انتخابی، همان نوع سفارشی مدنظر ما است. currentClaimValue دقیقا همان ID اکشن متد جاری است که توسط کنار هم قرار دادن area:controller:action تشکیل شده‌است.
متد HasClaim هیچگونه رفت و برگشتی را به بانک اطلاعاتی ندارد و اطلاعات خود را از کوکی شخص دریافت می‌کند. متد user.IsInRole نیز به همین نحو عمل می‌کند.


Tag Helper جدید SecurityTrimming

اکنون که سرویس ISecurityTrimmingService را پیاده سازی کرده‌ایم، از آن می‌توان جهت توسعه‌ی SecurityTrimmingTagHelper نیز استفاده کرد:
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            context.CheckArgumentIsNull(nameof(context));
            output.CheckArgumentIsNull(nameof(output));

            // don't render the <security-trimming> tag.
            output.TagName = null;

            if(_securityTrimmingService.CanCurrentUserAccess(Area, Controller, Action))
            {
                // fine, do nothing.
                return;
            }

            // else, suppress the output and generate nothing.
            output.SuppressOutput();
        }
عملکرد آن نیز بسیار ساده است. اگر کاربر، به area:controller:action جاری دسترسی داشت، این Tag Helper کاری را انجام نمی‌دهد. اگر خیر، متد SuppressOutput را فراخوانی می‌کند. این متد سبب خواهد شد، هر آنچه که داخل تگ‌های این TagHelper قرار گرفته، در صفحه رندر نشوند و از خروجی آن حذف شوند. به این ترتیب، کاربر به اطلاعاتی که به آن دسترسی ندارد (مانند لینک به مدخلی خاص را) مشاهده نخواهد کرد. به این مفهوم security trimming می‌گویند.
نمونه‌ای از کاربرد آن‌را در ReportsMenu.cshtml_ می‌توانید مشاهده کنید:
            <security-trimming asp-area="" asp-controller="DynamicPermissionsTest" asp-action="Products">
                <li>
                    <a asp-controller="DynamicPermissionsTest" asp-action="Products" asp-area="">
                        <span class="left5 fa fa-user" aria-hidden="true"></span>
                        گزارش از لیست محصولات
                    </a>
                </li>
            </security-trimming>
در اینجا اگر کاربر جاری به کنترلر DynamicPermissionsTest و اکشن متد Products آن دسترسی پویا نداشته باشد، محتوای قرارگرفته‌ی داخل تگ security-trimming را مشاهده نخواهد کرد.

برای آزمایش آن یک کاربر جدید را به سیستم DNT Identity اضافه کنید. سپس آن‌را در گروه نقشی مشخص قرار دهید (منوی مدیریتی،‌گزینه‌ی مدیریت نقش‌های سیستم). سپس به این گروه دسترسی به تعدادی از آیتم‌های پویا را بدهید (گزینه‌ی مشاهده و تغییر لیست دسترسی‌های پویا). سپس با این اکانت جدید به سیستم وارد شده و بررسی کنید که چه تعدادی از آیتم‌های منوی «گزارشات نمونه» را می‌توانید مشاهده کنید (تامین شده‌ی توسط ReportsMenu.cshtml_).


مدیریت اندازه‌ی حجم کوکی‌های ASP.NET Core Identity

همانطور که ملاحظه کردید، جهت بالابردن سرعت دسترسی به اطلاعات User Claims و Role Claims، تمام اطلاعات مرتبط با آن‌ها، به کوکی کاربر وارد شده‌ی به سیستم، اضافه می‌شوند. همین مساله در یک سیستم بزرگ با تعداد صفحات بالا، سبب خواهد شد تا حجم کوکی کاربر از 5 کیلوبایت بیشتر شده و توسط مرورگرها مورد قبول واقع نشوند و عملا سیستم از کار خواهد افتاد.
برای مدیریت یک چنین مساله‌ای، امکان ذخیره سازی کوکی‌های شخص در داخل بانک اطلاعاتی نیز پیش بینی شده‌است. زیر ساخت آن‌را در مطلب «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» پیشتر در این سایت مطالعه کردید و در پروژه‌ی DNT Identity بکارگرفته شده‌است.
اگر به کلاس IdentityServicesRegistry مراجعه کنید، یک چنین تنظیمی در آن قابل مشاهده است:
 var ticketStore = provider.GetService<ITicketStore>();
identityOptionsCookies.ApplicationCookie.SessionStore = ticketStore; // To manage large identity cookies
در ASP.NET Identity Core، امکان تدارک SessionStore سفارشی برای کوکی‌ها نیز وجود دارد. این SessionStore  باید با پیاده سازی اینترفیس ITicketStore تامین شود. دو نمونه پیاده سازی ITicketStore را در لایه سرویس‌های پروژه می‌توانید مشاهده کنید:
الف) DistributedCacheTicketStore
ب) MemoryCacheTicketStore

اولی از همان زیرساخت «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» استفاده می‌کند و دومی از IMemoryCache توکار ASP.NET Core برای پیاده سازی مکان ذخیره سازی محتوای کوکی‌های سیستم، بهره خواهد برد.
باید دقت داشت که اگر حالت دوم را انتخاب کنید، با شروع مجدد برنامه، تمام اطلاعات کوکی‌های کاربران نیز حذف خواهند شد. بنابراین استفاده‌ی از حالت ذخیره سازی آن‌ها در بانک اطلاعاتی منطقی‌تر است.


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


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
CheckBoxList برای فیلد Enum Flags مدل در ASP.Net MVC
قبلا مطالبی در سایت راجع به نوع داده شمارشی یا Enum و همچنین CheckBoxList و RadioButtonList وجود دارد. اما در این مطلب قصد دارم تا یک روش متفاوت را برای تولید و بهره گیری از CheckBoxList با استفاده از نوع داده‌های شمارشی برای شما ارائه کنم.
فرض کنید بخواهید به کاربر این امکان را بدهید تا بتواند چندین گزینه را برای یک فیلد انتخاب کند. به عنوان یک مثال ساده فرض کنید گزینه ای از مدل، پارچه‌های مورد علاقه یک نفر هست. کاربر می‌تواند چندین پارچه را انتخاب کند. و این فرض را هم بکنید که به لیست پارچه‌ها گزینه دیگری اضافه نخواهد شد. پارچه (Fabric) را مثلا می‌توانیم به صورت زیر تقسیم بندی کنیم :
  1. پنبه (Cotton)
  2. ابریشم (Silk)
  3. پشم (Wool)
  4. ابریشم مصنوعی (Rayon)
  5. پارچه‌های دیگر (Other)

با توجه به اینکه دیگر قرار نیست به این لیست گزینه دیگری اضافه شود می‌توانیم آنرا به صورت یک نوع داده شمارشی (Enum) تعریف کنیم. مثلا بدین صورت:

public enum Fabric
{
    [Description("پنبه")]
    Cotton,

    [Description("ابریشم")]
    Silk,

    [Description("پشم")]
    Wool,

    [Description("ابریشم مصنوعی")]
    Rayon,

    [Description("پارچه‌های دیگر")]
    Other
}

حال فرض کنید View Model زیر فیلدی از نوع نوع داده شمارشی Fabric دارد:
public class MyViewModel
{
    public Fabric Fabric { get; set; }
}

توجه داشته باشید که فیلد Fabric از کلاس MyViewModel باید چند مقدار را در خود نگهداری کند. یعنی می‌تواند هر کدام از گزینه‌های Cotton، Silk، Wool، Rayon، Other به صورت جداگانه یا ترکیبی باشد. اما در حال حاضر با توجه به اینکه یک فیلد Enum معمولی فقط می‌تواند یک مقدار را در خودش ذخیره کند قابلیت ذخیره ترکیبی مقادیر در فیلد Fabric از View Model بالا وجود ندارد.

اما راه حل این مشکل استفاده از پرچم (Flags) در تعریف نوع داده شمارشی هست. با استفاده از پرچم نوع داده شمارشی بالا به صورت زیر باید تعریف شود:
[Flags]
public enum Fabric
{
    [Description("پنبه")]
    Cotton = 1,

    [Description("ابریشم")]
    Silk = 2,

    [Description("پشم")]
    Wool = 4,

    [Description("ابریشم مصنوعی")]
    Rayon = 8,

    [Description("پارچه‌های دیگر")]
    Other = 128
}
همان طور که می‌بینید از عبارت [Flags] قبل از تعریف enum استفاده کرده ایم. همچنین هر کدام از مقادیر ممکن این نوع داده شمارشی با توانهایی از 2 تنظیم شده اند. در این صورت یک نمونه از این نوع داده می‌تواند چندین مقدار را در خودش ذخیره کند.

برای آشنایی بیشتر با این موضوع به کدهای زیر نگاه کنید:
Fabric cotWool = Fabric.Cotton | Fabric.Wool;
int cotWoolValue = (int) cotWool;
به وسیله عملگر | می‌توان چندین مقدار را در یک نمونه از نوع Fabric ذخیره کرد. مثلا متغیر cotWool هم دارای مقدار Fabric.Cotton و هم دارای مقدار Fabric.Wool هست. مقدار عددی معادل متغیر cotWool برابر 5 هست که از جمع مقدار عددی Fabric.Cotton و Fabric.Wool به دست آمده است.

حال فرض کنید فیلد Fabric از View Model ذکر شده (کلاس MyViewModel) را به صورت لیستی از چک باکس‌ها نمایش دهیم. مثل زیر:

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

برای این کار از پروژه MVC Enum Flags کمک خواهیم گرفت. این پروژه شامل یک Html Helper برای تبدیل یه Enum به یک CheckBoxList و همچنین شامل Model Binder مربوطه هست. البته بعضی از کدهای Html Helper آن احتیاج به تغییر داشت که آنرا انجام دادم ولی بایندر آن بسیار خوب کار می‌کند.

خوب html helper مربوط به آن به صورت زیر می‌باشد:
public static IHtmlString CheckBoxesForEnumFlagsFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
    ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
    Type enumModelType = metadata.ModelType;

    // Check to make sure this is an enum.
    if (!enumModelType.IsEnum)
    {
        throw new ArgumentException("This helper can only be used with enums. Type used was: " + enumModelType.FullName.ToString() + ".");
    }

    // Create string for Element.
    var sb = new StringBuilder();

    foreach (Enum item in Enum.GetValues(enumModelType))
    {
        if (Convert.ToInt32(item) != 0)
        {
            var ti = htmlHelper.ViewData.TemplateInfo;
            var id = ti.GetFullHtmlFieldId(item.ToString());

            //Derive property name for checkbox name
            var body = expression.Body as MemberExpression;
            var propertyName = body.Member.Name;
            var name = ti.GetFullHtmlFieldName(propertyName);
                    
            //Get currently select values from the ViewData model
            TEnum selectedValues = expression.Compile().Invoke(htmlHelper.ViewData.Model);

            var label = new TagBuilder("label");
            label.Attributes["for"] = id;
            label.Attributes["style"] = "display: inline-block;";
            var field = item.GetType().GetField(item.ToString());

            // Add checkbox.
            var checkbox = new TagBuilder("input");
            checkbox.Attributes["id"] = id;
            checkbox.Attributes["name"] = name;
            checkbox.Attributes["type"] = "checkbox";
            checkbox.Attributes["value"] = item.ToString();
                    
            if ((selectedValues as Enum != null) && ((selectedValues as Enum).HasFlag(item)))
            {
                checkbox.Attributes["checked"] = "checked";
            }
            sb.AppendLine(checkbox.ToString());

            // Check to see if DisplayName attribute has been set for item.
            var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true)
                .FirstOrDefault() as DisplayNameAttribute;
            if (displayName != null)
            {
                // Display name specified.  Use it.
                label.SetInnerText(displayName.DisplayName);
            }
            else
            {
                // Check to see if Display attribute has been set for item.
                var display = field.GetCustomAttributes(typeof(DisplayAttribute), true)
                    .FirstOrDefault() as DisplayAttribute;
                if (display != null)
                {
                    label.SetInnerText(display.Name);
                }
                else
                {
                    label.SetInnerText(item.ToDescription());
                }
            }
            sb.AppendLine(label.ToString());

            // Add line break.
            sb.AppendLine("<br />");
        }
    }

    return new HtmlString(sb.ToString());
}

در کدهای بالا از متد الحاقی ToDescription نیز برای تبدیل معادل انگلیسی به فارسی یک مقدار از نوع داده شمارشی استفاده کرده ایم.
public static string ToDescription(this Enum value)
{
    var attributes = (DescriptionAttribute[])value.GetType().GetField(value.ToString()).GetCustomAttributes(typeof(DescriptionAttribute), false);
    return attributes.Length > 0 ? attributes[0].Description : value.ToString();
}

برای استفاده از این Html Helper در View کد زیر را می‌نویسیم:
@Html.CheckBoxesForEnumFlagsFor(x => x.Fabric)

که باعث تولید خروجی که در تصویر (الف) نشان داده شد می‌شود. و همچنین مدل بایندر مربوط به آن به صورت زیر هست:
public class FlagEnumerationModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext == null) throw new ArgumentNullException("bindingContext");

        if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
        {
            var values = GetValue<string[]>(bindingContext, bindingContext.ModelName);

            if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false)))
            {
                long byteValue = 0;
                foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v)))
                {
                    byteValue |= (int)Enum.Parse(bindingContext.ModelType, value);
                }

                return Enum.Parse(bindingContext.ModelType, byteValue.ToString());
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }

    private static T GetValue<T>(ModelBindingContext bindingContext, string key)
    {
        if (bindingContext.ValueProvider.ContainsPrefix(key))
        {
            ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key);
            if (valueResult != null)
            {
                bindingContext.ModelState.SetModelValue(key, valueResult);
                return (T)valueResult.ConvertTo(typeof(T));
            }
        }
        return default(T);
    }
}

این مدل بایندر را باید به این صورت در متد Application_Start فایل Global.asax فراخوانی کنیم:
ModelBinders.Binders.Add(typeof(Fabric), new FlagEnumerationModelBinder());

مشاهده می‌کنید که در اینجا دقیقا مشخص کرده ایم که این مدل بایندر برای نوع داده شمارشی Fabric هست. اگر نیاز دارید تا این بایندر برای نوع داده‌های شمارشی دیگری نیز به کار رود نیاز هست تا این خط کد را برای هر کدام از آنها تکرار کنید. اما راه حل بهتر این هست که کلاسی به صورت زیر تعریف کنیم و تمامی نوع داده‌های شمارشی که باید از بایندر بالا استفاده کنند را در یک پراپرتی آن برگشت دهیم. مثلا بدین صورت:
public class ModelEnums
{
    public static IEnumerable<Type> Types
    {
        get
        {
            var types = new List<Type> { typeof(Fabric) };
            return types;
        }
    }
}

سپس به متد Application_Start رفته و کد زیر را اضافه می‌کنیم:
foreach (var type in ModelEnums.Types)
{
   ModelBinders.Binders.Add(type, new FlagEnumerationModelBinder())
}

اگر گزینه‌های پشم و ابریشم مصنوعی را از CheckBoxList تولید شده انتخاب کنیم، بدین صورت:

شکل (ب)


و سپس فرم را پست کنید، موردی شبیه زیر مشاهده می‌کنید:

شکل (ج)

همچنین مقدار عددی معادل در این جا برابر 12 می‌باشد که از جمع دو مقدار Wool و Rayon به دست آمده است. بدین ترتیب در یک فیلد از مدل، گزینه‌های انتخابی توسط کاربر قرار گرفته شده اند.

پروژه مربوط به این مثال را از لینک زیر دریافت کنید:
MvcEnumFlagsProjectSample.zip

پی نوشت : پوشه‌های bin و obj و packages جهت کاهش حجم پروژه از آن حذف شده اند. برای بازسازی پوشه packages لطفا به مطلب بازسازی کامل پوشه packages بسته‌های NuGet به صورت خودکار مراجعه کنید.
مطالب
نمایش HTML در برنامه‌های Angular
فرض کنید قصد داریم خاصیت htmlContent زیر را در قالب این کامپوننت نمایش دهیم:
export class ShowHtmlComponent {
  htmlContent = "Template <script>alert(\"Hello!\")</script> <b>Syntax</b>";
}
اگر از روش متداول binding استفاده شود:
<h3>Binding innerHTML</h3>
<p>Bound value:</p>
<p>{{htmlContent}}</p>
چنین خروجی حاصل خواهد شد:


همچنین اگر به کنسول developer tools مرورگر مراجعه کنیم، چنین اخطاری نیز درج شده است:
 WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).
به این معنا که Angular به صورت توکار تمام خروجی‌ها را به صورت encode شده نمایش می‌دهد و در مقابل حملات XSS مقاوم است. Sanitizing نیز در اینجا به معنای تغییر ورودی و تبدیل آن به مقداری است که جهت درج در DOM امن است.


روش نمایش HTML در برنامه‌های Angular

اما اگر خواستیم اطلاعات HTML ایی را به همان صورتی که هستند نمایش دهیم چطور؟ در این حالت باید از روش ویژه‌ی ذیل استفاده کرد:
<p>Result of binding to innerHTML:</p>
<p [innerHTML]="htmlContent"></p>
برای نمایش HTML نیاز است آن‌را به ویژگی innerHTML متصل کرد؛ با این خروجی:


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

روش دیگر کار با innerHTML، تعریف یک template reference variable در قالب کامپوننت است:
<p #dataContainer></p>
و سپس دسترسی به آن از طریق یک ViewChild و انتساب مقداری بهinnerHTML  آن به صورت ذیل:
export class ShowHtmlComponent implements OnInit {

  @ViewChild("dataContainer") dataContainer: ElementRef;

  ngOnInit() {
    this.dataContainer.nativeElement.innerHTML = "nativeElement <script>alert(\"Hello!\")</script> <b>Syntax</b>";
  }
}
با این خروجی:


که اینبار قسمت script آن به طور کامل حذف شده‌است.


حالات مختلفی که Angular برنامه را از حملات XSS محافظت می‌کند

در ذیل، لیست مواردی را مشاهده می‌کنید که به صورت پیش‌فرض توسط Angular در مقابل حملات XSS محافظت می‌شوند و اطلاعات انتساب داده شده‌ی به آن‌ها تمیزسازی خواهند شد:
HTML 
Attributes – 
<div [innerHTML]="UNTRUSTED"></div> 
OR <input value="UNTRUSTED">

Style— 
<div [style]="height:UNTRUSTED"></div>

URL — 
<a [href]="UNTRUSTED-URL"></a> 
OR <script [src]="UNTRUSTED-URL"></script> 
OR <iframe src="UNTRUSTED-URL" />

GET Parameter – 
<a href="/user?id=UNTRUSTED">link</a>

JavaScript Variable –
<script> var value='UNTRUSTED';</script>


تبدیل کردن یک HTML نا امن ورودی به یک HTML امن در Angular

بهتر است اطلاعات دریافتی از کاربران پیش از ارسال به سرور تمیز شوند. برای این منظور می‌توان از سرویس ویژه‌ای به نام DomSanitizer کمک گرفت. کار این سرویس، امن سازی اطلاعات نمایش داده شده‌ی در برنامه‌های Angular است.
export class ShowHtmlComponent implements OnInit {
  sanitizedHtml: string;

  constructor(private sanitizer: DomSanitizer) { }

  ngOnInit() {
    this.sanitizedHtml = this.sanitizer.sanitize(SecurityContext.HTML, "<b>Sanitize</b><script>attackerCode()</script>");
  }
}
در این حالت سرویس DomSanitizer به سازنده‌ی کلاس تزریق شده و سپس می‌توان از متدهای مختلف آن مانند sanitize استفاده کرد. خروجی آن صرفا حذف تگ اسکریپت و نگهداری کدهای درون آن است.


در این حالت می‌توان موارد ذیل را کنترل کرد. برای مثال اگر مقدار دریافتی CSS است، می‌توان از SecurityContext.STYLE استفاده کرد و سایر حالات آن مانند امن سازی HTML، اسکریپت و آدرس‌های اینترنتی به شرح ذیل هستند:
SecurityContext.NONE
SecurityContext.HTML
SecurityContext.STYLE
SecurityContext.SCRIPT
SecurityContext.URL
SecurityContext.RESOURCE_URL


غیرفعال کردن سیستم امنیتی Angular جهت نمایش کامل یک مقدار HTML ایی

اگر خواستیم اطلاعات HTML ایی را با فرض امن بودن آن، به همان نحوی که هست نمایش دهیم چطور؟
سرویس DomSanitizer شامل متدهای ذیل نیز می‌باشد:
export enum SecurityContext { NONE, HTML, STYLE, SCRIPT, URL, RESOURCE_URL }

export abstract class DomSanitizer implements Sanitizer {
  abstract sanitize(context: SecurityContext, value: SafeValue|string|null): string|null;
  abstract bypassSecurityTrustHtml(value: string): SafeHtml;
  abstract bypassSecurityTrustStyle(value: string): SafeStyle;
  abstract bypassSecurityTrustScript(value: string): SafeScript;
  abstract bypassSecurityTrustUrl(value: string): SafeUrl;
  abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl;
}
اولین متد آن sanitize است که در مورد آن توضیح داده شد. سایر متدها، کار غیرفعال سازی سیستم امنیتی توکار Angular را انجام می‌دهند.
برای کار با آن‌ها همانند مثال استفاده‌ی از متد sanitize می‌توان سرویس DomSanitizer را به سازنده‌ی یک کامپوننت تزریق کرد و یا می‌توان این عملیات تکراری فرمت اطلاعات ورودی را تبدیل به یک Pipe جدید کرد:
import { Pipe, PipeTransform } from "@angular/core";
import { DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl } from "@angular/platform-browser";

@Pipe({
  name: "safe"
})
export class SafePipe implements PipeTransform {
  constructor(protected sanitizer: DomSanitizer) { }

  public transform(value: any, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
    switch (type) {
      case "html":
        return this.sanitizer.bypassSecurityTrustHtml(value);
      case "style":
        return this.sanitizer.bypassSecurityTrustStyle(value);
      case "script":
        return this.sanitizer.bypassSecurityTrustScript(value);
      case "url":
        return this.sanitizer.bypassSecurityTrustUrl(value);
      case "resourceUrl":
        return this.sanitizer.bypassSecurityTrustResourceUrl(value);
      default:
        throw new Error(`Invalid safe type specified: ${type}`);
    }
  }
}
کار این Pipe غیرفعال کردن سیستم امنیتی Angular و نمایش html، style و غیره به همان صورتی که هستند، می‌باشد.
برای استفاده‌ی از آن، ابتدا این Pipe به قسمت declarations ماژول مدنظر اضافه خواهد شد:
@NgModule({
  imports: [
  // ...
  ],
  declarations: [ SafePipe]
})
و سپس در قالب کامپوننت به نحو ذیل می‌توان با آن کار کرد:
<p [innerHTML]="htmlContent | safe: 'html'"></p>
در این حالت متد bypassSecurityTrustHtml بر روی htmlContent، فراخوانی شده و نتیجه‌ی نهایی نمایش داده خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
React 16x - قسمت 16 - مسیریابی - بخش 2 - پارامترهای مسیریابی
در قسمت قبل، نحوه‌ی برپایی و تنظیمات اولیه‌ی کتابخانه‌ی مسیریابی react-router-dom را بررسی کردیم. در ادامه نحوه‌ی دریافت پارامترهای مسیریابی و سایر جزئیات این کتابخانه را مرور می‌کنیم.


دریافت پارامترهای مسیریابی

گاهی از اوقات نیاز به ارسال پارامترهایی به مسیریابی‌های تعریف شده‌است. برای مثال در لیست محصولات تعریف شده، بسته به انتخاب هر کدام، باید id متناظری نیز در URL نهایی ظاهر شود. به این Id، یک Route parameter گفته می‌شود. برای پیاده سازی این نیازمندی به صورت زیر عمل می‌کنیم:
در فایل app.js، یک مسیریابی جدید را برای نمایش جزئیات یک محصول اضافه می‌کنیم:
import ProductDetails from "./components/productDetails";
// ...
class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Switch>
            <Route path="/products/:id" component={ProductDetails} />
            <Route
              path="/products"
              render={props => (
                <Products param1="123" param2="456" {...props} />
              )}
            />
            <Route path="/posts/:year/:month" component={Posts} />
            <Route path="/admin" component={Dashboard} />
            <Route path="/" component={Home} />
          </Switch>
        </div>
      </div>
    );
  }
}
در اینجا برای تعریف یک پارامتر مسیریابی، آن‌را با : شروع می‌کنیم؛ مانند id:، در مسیریابی جدید فوق. البته امکان تعریف چندین پارامتر هم در اینجا وجود دارد؛ مانند تعریف پارامترهای سال و ماه برای مسیریابی مطالب. به علاوه چون این نوع مسیریابی‌ها ویژه‌تر هستند، باید در ابتدا قرارگیرند. برای مثال اگر مسیریابی products/ را در اول لیست قرار دهیم، دیگر کار به انتخاب products/:id/ نخواهد رسید.

کامپوننت جدید src\components\productDetails.jsx نیز به صورت زیر تعریف شده‌است:
import React, { Component } from "react";

class ProductDetails extends Component {
  handleSave = () => {
    // Navigate to /products
  };

  render() {
    return (
      <div>
        <h1>Product Details - </h1>
        <button className="btn btn-primary" onClick={this.handleSave}>
      </div>
    );
  }
}

export default ProductDetails;
پس از این تغییرات و ذخیره سازی برنامه، با بارگذاری مجدد برنامه در مرورگر، ابتدا صفحه‌ی products را از منوی راهبری سایت انتخاب کرده و سپس بر روی یکی از محصولات لیست شده کلیک می‌کنیم. سپس در افزونه‌ی react developer tools، کامپوننت نمایش داده شده‌ی ProductDetails را انتخاب می‌کنیم:


در اینجا با گشودن اطلاعات خاصیت match تزریق شده‌ی به کامپوننت ProductDetails، می‌توان اطلاعاتی مانند پارامترهای دریافتی مسیریابی را دقیقا مشاهده کرد. برای مثال در این تصویر id=1 از URL بالای صفحه که به http://localhost:3000/products/1 تنظیم شده‌است، دریافت می‌شود.
بنابراین امکان خواندن اطلاعات پارامترهای مسیریابی، توسط شیء match تزریق شده‌ی به یک کامپوننت وجود دارد. به همین جهت کامپوننت ProductDetails را ویرایش کرده و المان h1 آن‌را جهت نمایش id محصول به صورت زیر تغییر می‌دهیم که در آن شیء match.params، از props کامپوننت تامین می‌شود:
<h1>Product Details - {this.props.match.params.id} </h1>

برای آزمایش آن مجددا از صفحه‌ی products شروع کرده و بر روی لینک یکی از محصولات، کلیک کنید. در اینجا هرچند id محصول به درستی نمایش داده می‌شود، اما ... نمایش جزئیات آن به همراه بارگذاری کامل و مجدد صفحه‌ی آن است که از حالت SPA خارج شده‌است. برای رفع این مشکل به کامپوننت products مراجعه کرده و anchor‌های تعریف شده را همانطور که در قسمت قبل نیز بررسی کردیم، تبدیل به کامپوننت Link می‌کنیم.
از حالت قبلی:
{this.state.products.map(product => (
  <li key={product.id}>
    <a href={`/products/${product.id}`}>{product.name}</a>
  </li>
))}
به حالت جدید:
import { Link } from "react-router-dom";
// ...

<Link to={`/products/${product.id}`}>{product.name}</Link>
با این تغییر دیگر در حین نمایش یک کامپوننت، بارگذاری کامل صفحه رخ نمی‌دهد.


پارامترهای اختیاری مسیریابی

به تعریف مسیریابی زیر، دو پارامتر سال و ماه، اضافه شده‌اند:
<Route path="/posts/:year/:month" component={Posts} />
و برای مثال اگر بر روی لینک posts در منوی راهبری کلیک کنیم، آدرسی مانند http://localhost:3000/posts/2018/06 ایجاد شده و سپس کامپوننت Posts رندر می‌شود. حال اگر پارامتر ماه را حذف کنیم http://localhost:3000/posts/2018 چه اتفاقی رخ می‌دهد؟ در این حالت برنامه کامپوننت Home را نمایش خواهد داد. علت اینجا است که پارامترهای تعریف شده‌ی در مسیریابی، به صورت پیش‌فرض اجباری هستند. به همین جهت URL وارد شده، چون با الگوی تعریفی Route فوق بدلیل نداشتن قسمت ماه، انطباق نیافته و تنها مسیریابی / که کامپوننت Home را نمایش می‌دهد، با آن تطابق یافته‌است.
برای رفع این مشکل می‌توان با اضافه کردن یک ? به هر پارامتر، پارامترهای تعریف شده را اختیاری کرد:
<Route path="/posts/:year?/:month?" component={Posts} />
در regexهای جاوا اسکریپتی زمانیکه یک ? را به یک عبارت باقاعده اضافه می‌کنیم، یعنی آن عبارت اختیاری است.

با این تغییرات اگر مجددا آدرس http://localhost:3000/posts/2018 را درخواست کنیم، کامپوننت Posts بجای کامپوننت Home نمایش داده می‌شود.

اکنون کامپوننت Posts را به صورت زیر تغییر می‌دهیم تا پارامترهای مسیریابی را نیز درج کند:
import React from "react";

const Posts = ({ match }) => {
  return (
    <div>
      <h1>Posts</h1>
      Year: {match.params.year} , Month: {match.params.month}
    </div>
  );
};

export default Posts;
پارامتر ({ match }) در اینجا به این معنا است که شیء props ارسالی به آن، توسط Object Destructuring تجزیه شده و خاصیت match آن در اینجا به صورت یک پارامتر در اختیار کامپوننت بدون حالت تابعی قرار گرفته‌است.

پس از ذخیره سازی این تغییرات و بارگذاری مجدد برنامه در مرورگر، اگر آدرس http://localhost:3000/posts/2018/1 را وارد کنیم، خروجی زیر حاصل می‌شود:



کار با پارامترهای کوئری استرینگ‌های مسیریابی

پارامترهای اختیاری، جزو قابلیت‌هایی هستند که باید تا حد ممکن از بکارگیری آن‌ها اجتناب و آن‌ها را با کوئری استرینگ‌ها تعریف کرد. کوئری استرینگ‌ها با یک ? در انتهای URL شروع می‌شوند و می‌توانند چندین پارامتر را داشته باشند؛ مانند: http://localhost:3000/posts?sortBy=newest&approved=true و یا حتی می‌توان آن‌ها را با پارامترهای اختیاری نیز ترکیب کرد مانند: http://localhost:3000/posts/2018/05?sortBy=newest&approved=true
برای استخراج کوئری استرینگ‌ها در برنامه‌های React باید از شیء location استفاده کرد:


در اینجا مقدار خاصیت search، کل قسمت کوئری استرینگ‌ها را به همراه دارد. البته ما قصد پردازش آن‌را به صورت دستی نداریم. به همین جهت از کتابخانه‌ی زیر برای انجام اینکار استفاده خواهیم کرد:
> npm i query-string --save
پس از نصب کتابخانه‌ی بسیار معروف query-string، به کامپوننت Posts مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
import queryString from "query-string";

const Posts = ({ match, location }) => {
  const result = queryString.parse(location.search);
  console.log(result);
  // ...
- پیشتر ذکر پارامتر ({ match }) را بررسی کردیم. در اینجا خاصیت location نیز به آن اضافه شده‌است تا پس از Object Destructuring شیء props ارسالی به کامپوننت، بتوان به مقدار شیء location نیز دسترسی یافت.
- سپس شیء queryString را از ماژول مرتبط، import می‌کنیم. در ادامه به کمک متد parse آن، می‌توان location.search را آنالیز کرد که خروجی آن، یک شیء جاوا اسکریپتی به صورت زیر است:
{approved: "true", sortBy: "newest"}
بنابراین در اینجا هم می‌توان توسط Object Destructuring، به این خواص دسترسی یافت:
 const { approved, sortBy } = queryString.parse(location.search);

یک نکته: باید دقت داشت که کتابخانه‌ی query-string، همیشه مقادیر خواص را رشته‌ای بازگشت می‌دهد؛ حتی اگر عدد باشند.


مدیریت مسیرهای نامعتبر درخواستی

فرض کنید کاربری آدرس غیرمعتبر http://localhost:3000/xyz را که هیچ نوع مسیریابی را برای آن تعریف نکرده‌ایم، درخواست می‌کند. در این حالت برنامه کامپوننت home را رندر می‌کند که مرتبط است با تعاریف مسیریابی برنامه در فایل app.js. تنها path تعریف شده‌ای که با این آدرس تطابق پیدا می‌کند، / متناظر با کامپوننت home است.
بجای این رفتار پیش‌فرض، مایل هستیم که کاربر به یک صفحه‌ی سفارشی «پیدا نشد» هدایت شود. به همین جهت ابتدا کامپوننت جدید تابعی بدون حالت src\components\notFound.jsx را با محتوای زیر ایجاد می‌کنیم:
import React from "react";

const NotFound = () => {
  return <h1>Not Found</h1>;
};

export default NotFound;
سپس ابتدا به مسیریابی /، ویژگی exact را هم اضافه می‌کنیم تا دیگر بجز ریشه‌ی سایت، به مسیر دیگری پاسخ ندهد:
<Route path="/" exact component={Home} />
اکنون اگر مجددا مسیر xyz را درخواست کنیم، فقط کامپوننت NavBar در صفحه ظاهر می‌شود. برای بهبود این وضعیت و نمایش کامپوننت NotFound، مراحل زیر را طی می‌کنیم:
- ابتدا شیء Redirect را از react-router-dom باید import کنیم.
- همچنین import کامپوننت NotFound نیز باید ذکر شود.
- سپس پیش از مسیریابی کلی /، مسیریابی جدید not-found را که به کامپوننت NotFound اشاره می‌کند، اضافه می‌کنیم.
- اکنون در انتهای Switch تعریف شده (جائی که دیگر هیچ مسیریابی تعریف شده‌ای، با مسیر درخواستی کاربر، تطابق نداشته)، باید کامپوننت Redirect را جهت هدایت به این مسیریابی جدید، تعریف کرد:
import { Redirect, Route, Switch } from "react-router-dom";
//...
import NotFound from "./components/notFound";
//...

class App extends Component {
  render() {
    return (
      <div>
        <NavBar />
        <div className="container">
          <Switch>
            //...
            <Route path="/not-found" component={NotFound} />
            <Route path="/" exact component={Home} />
            <Redirect to="/not-found" />
          </Switch>
        </div>
      </div>
    );
  }
}
پس از این تغییرات، اگر آدرس نامعتبر http://localhost:3000/xyz درخواست شود، بلافاصله به آدرس http://localhost:3000/not-found هدایت می‌شویم.

کاربرد دیگر کامپوننت Redirect، هدایت کاربران از یک آدرس قدیمی، به یک آدرس جدید است که نحوه‌ی تعریف آن به صورت زیر می‌باشد:
<Redirect from="/messages" to="/posts" />
با این تنظیم اگر کاربری مسیر http://localhost:3000/messages را درخواست دهد، به صورت خودکار به http://localhost:3000/posts هدایت خواهد شد.


هدایت کاربران به آدرس‌های مختلف با برنامه نویسی

گاهی از اوقات پس از تکمیل فرمی و یا کلیک بر روی دکمه‌ای، می‌خواهیم کاربر را به آدرس خاصی هدایت کنیم. برای مثال در برنامه‌ی جاری می‌خواهیم زمانیکه کاربری صفحه‌ی جزئیات یک محصول را مشاهده و بر روی دکمه‌ی فرضی Save کلیک کرد، دوباره به همان صفحه‌ی لیست محصولات هدایت شود؛ برای این منظور از شیء history استفاده خواهیم کرد:


در اینجا متدها و خواص شیء history را مشاهده می‌کنید. برای نمونه توسط متد push آن می‌توان آدرس جدیدی را به تاریخچه‌ی آدرس‌های مرور شده‌ی توسط کاربر، اضافه کرد و کاربر را با برنامه نویسی، به صفحه‌ی جدیدی هدایت نمود:
class ProductDetails extends Component {
  handleSave = () => {
    // Navigate to /products
    this.props.history.push("/products");
  };

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


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


قصد داریم به صفحه‌ی admin، دو لینک جدید به مطالب و کاربران ادمین را اضافه کنیم، به نحوی که با کلیک بر روی هر کدام، محتوای هر صفحه‌ی متناظر، در همینجا نمایش داده شود (تصویر فوق). به عبارتی در حال حاضر، مسیریابی تعریف شده، جهت مدیریت لینک‌های NavBar بالای صفحه کار می‌کند. اکنون می‌خواهیم مسیریابی دیگری را داخل آن برای پوشش منوی کنار صفحه‌ی ادمین اضافه کنیم. به اینکار، تعریف مسیریابی‌های تو در تو گفته می‌شود و پیاده سازی آن توسط کامپوننت react-router-dom بسیار ساده‌است و شامل این موارد می‌شود:
- ابتدا مسیریابی‌های جدید را همینجا داخل کامپوننت src\components\admin\dashboard.jsx تعریف می‌کنیم:
const Dashboard = ({ match }) => {
  return (
    <div>
      <h1>Admin Dashboard</h1>
      <div className="row">
        <div className="col-3">
          <SideBar />
        </div>
        <div className="col">
          <Route path="/admin/users" component={Users} />
          <Route path="/admin/posts" component={Posts} />
        </div>
      </div>
    </div>
  );
};
در اینجا محتوای کامپوننت بدون حالت تابعی Dashboard را ملاحظه می‌کنید که از یک کامپوننت منوی SideBar و سپس در ستونی دیگر، از 2 کامپوننت Route تشکیل شده‌است که بر اساس URL رسیده، سبب رندر کامپوننت‌های جدید Users و Posts خواهند شد.
تنها نکته‌ی جدید آن، امکان درج کامپوننت Route در قسمت‌های مختلف برنامه، خارج از app.js می‌باشد و این امکان محدود به app.js نیست. در این حالت اگر مسیر /admin/posts توسط کاربر وارد شد، دقیقا در همان محلی که کامپوننت Route درج شده‌است، کامپوننت متناظر با این مسیر یعنی کامپوننت Posts، رندر می‌شود.

در ادامه محتوای این کامپوننت‌های جدید را نیز ملاحظه می‌کنید:
محتوای کامپوننت src\components\admin\sidebar.jsx
import React from "react";
import { Link } from "react-router-dom";

const SideBar = () => {
  return (
    <ul className="list-group">
      <li className="list-group-item">
        <Link to="/admin/posts">Posts</Link>
      </li>
      <li className="list-group-item">
        <Link to="/admin/users">Users</Link>
      </li>
    </ul>
  );
};

export default SideBar;

محتوای کامپوننت src\components\admin\users.jsx
import React from "react";

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

export default Users;

محتوای کامپوننت src\components\admin\posts.jsx
import React from "react";

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

export default Posts;


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-15-part-02.zip
مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
یکی دیگر از ویجت‌های Kendo UI یک HTML Editor کامل است به همراه امکانات ارسال فایل، تصویر و ... پشتیبانی از راست به چپ. در ادامه قصد داریم نحوه‌ی مدیریت نمایش لیست فایل‌ها، افزودن و حذف آن‌ها را از طریق این ادیتور بررسی کنیم.


تنظیمات ابتدایی Kendo UI Editor

در ذیل کدهای سمت کاربر فعال سازی مقدماتی Kendo UI را مشاهده می‌کنید. در قسمت tools آن، لیست امکانات و نوار ابزار مهیای آن درج شده‌اند.
دو مورد insertImage و insertFile آن نیاز به تنظیمات سمت کاربر و سرور بیشتری دارند.
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl">
    <textarea id="editor" rows="10" cols="30" style="height: 440px"></textarea>
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#editor").kendoEditor({
                tools: [
                    "bold", "italic", "underline", "strikethrough", "justifyLeft",
                    "justifyCenter", "justifyRight", "justifyFull", "insertUnorderedList",
                    "insertOrderedList", "indent", "outdent", "createLink", "unlink",
                    "insertImage", "insertFile",
                    "subscript", "superscript", "createTable", "addRowAbove", "addRowBelow",
                    "addColumnLeft", "addColumnRight", "deleteRow", "deleteColumn", "viewHtml",
                    "formatting", "cleanFormatting", "fontName", "fontSize", "foreColor",
                    "backColor", "print"
                ],
                imageBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
                            type: "POST"
                        },
                        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
                        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
                    }
                },
                fileBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorFiles")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorFiles")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorFiles")",
                            type: "POST"
                        },
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorFiles")",
                        fileUrl: "@Url.Action("GetFile", "KendoEditorFiles")?path={0}"
                    }
                }
            });
        });
    </script>
}
در اینجا نحوه‌ی تنظیم مسیرهای مختلف ارسال فایل و تصویر Kendo UI Editor را ملاحظه می‌کنید.
منهای قسمت thumbnailUrl، عملکرد قسمت‌های مختلف افزودن فایل و تصویر این ادیتور یکسان هستند. به همین جهت می‌توان برای مثال کنترلی مانند KendoEditorFilesController را ایجاد و سپس در کنترلر KendoEditorImagesController از آن ارث بری کرد و متد دریافت و نمایش بند انگشتی تصاویر را افزود. به این ترتیب دیگر نیازی به تکرار کدهای مشترک بین این دو قسمت نخواهد بود.


نمایش لیست پوشه‌ها و تصویر در ابتدای باز شدن صفحه‌ی درج تصویر

با کلیک بر روی دکمه‌ی نمایش لیست تصاویر، صفحه دیالوگی مانند شکل زیر ظاهر خواهد شد:


تنظیمات خواندن این فایل‌ها، از قسمت read مربوط به imageBrowser دریافت می‌شود که cache آن نیز به false تنظیم شده‌است تا در این بین مرورگر اطلاعات را کش نکند. این مورد در حین حذف فایل‌ها و پوشه‌ها مهم است. زیرا اگر cache:false تنظیم نشده باشد، حذف یک فایل یا پوشه در سمت کاربر تاثیری نخواهد داشت.
imageBrowser: {
    transport: {
        read: {
            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
            dataType: "json",
            contentType: 'application/json; charset=utf-8',
            type: 'GET',
            cache: false
        }
    }
},
در ادامه نیاز است اکشن متد GetFilesList را به نحو ذیل در سمت سرور تهیه کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpGet]
        public ActionResult GetFilesList(string path)
        {
            path = GetSafeDirPath(path);
            var imagesList = new DirectoryInfo(path)
                                .GetFiles()
                                .Select(fileInfo => new KendoFile
                                {
                                    Name = fileInfo.Name,
                                    Size = fileInfo.Length,
                                    Type = KendoFileType
                                }).ToList();
 
            var foldersList = new DirectoryInfo(path)
                                .GetDirectories()
                                .Select(directoryInfo => new KendoFile
                                {
                                    Name = directoryInfo.Name,
                                    Type = KendoDirType
                                }).ToList();
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(imagesList.Union(foldersList), new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
 
 
        protected string GetSafeDirPath(string path)
        {
            // path = مسیر زیر پوشه‌ی وارد شده
            if (string.IsNullOrWhiteSpace(path))
            {
                return Server.MapPath(FilesFolder);
            }
 
            //تمیز سازی امنیتی
            path = Path.GetDirectoryName(path);
            path = Path.Combine(Server.MapPath(FilesFolder), path);
            return path;
        } 
    }
}
در اینجا کدهای کلاس پایه KendoEditorFilesController را مشاهده می‌کنید. به این جهت فیلد FilesFolder آن protected تعریف شده‌است تا در کلاسی که از آن ارث بری می‌کند نیز قابل دسترسی باشد. سپس لیست فایل‌ها و پوشه‌های path دریافتی با فرمت لیستی از KendoFile تهیه شده و با فرمت JSON بازگشت داده می‌شوند. ساختار KendoFile را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Models
{
    public class KendoFile
    {
        public string Name { set; get; }
        public string Type { set; get; }
        public long Size { set; get; }
    }
}
- در اینجا Type می‌تواند از نوع فایل با مقدار f و یا از نوع پوشه با مقدار d باشد.
- علت استفاده از CamelCasePropertyNamesContractResolver در حین بازگشت JSON نهایی، تبدیل خواص دات نتی، به نام‌های سازگار با JavaScript است. برای مثال به صورت خودکار Name را تبدیل به name می‌کند.
- پارامتر path در ابتدای کار خالی است. اما کاربر می‌تواند در بین پوشه‌های باز شده‌ی توسط مرورگر تصاویر Kendo UI حرکت کند. به همین جهت مقدار آن باید هربار بررسی شده و بر این اساس لیست فایل‌ها و پوشه‌های جاری بازگشت داده شوند.


مدیریت حذف تصاویر و پوشه‌ها

همانطور که در شکل فوق نیز مشخص است، با انتخاب یک پوشه یا فایل، دکمه‌ای با آیکن ضربدر جهت فراهم آوردن امکان حذف، ظاهر می‌شود. این دکمه متصل است به قسمت destroy تنظیمات ادیتور:
imageBrowser: {
    transport: {
        destroy: {
            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
            type: "POST"
        }
    }
},
این تنظیمات سمت کاربر را باید به نحو ذیل در سمت سرور مدیریت کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult DestroyFile(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
 
            var pathToDelete = Path.Combine(path, name);
 
            var attr = System.IO.File.GetAttributes(pathToDelete);
            if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
            {
                Directory.Delete(pathToDelete, recursive: true);
            }
            else
            {
                System.IO.File.Delete(pathToDelete);
            }
 
            return Json(new object[0]);
        } 
    }
}
- استفاده از Path.GetFileName جهت دریافت نام فایل‌ها در اینجا بسیار مهم است. زیرا اگر این تمیز سازی امنیتی صورت نگیرد، ممکن است با کمی تغییر در آن، فایل web.config برنامه، دریافت یا حذف شود.
- پارامتر name دریافتی مساوی است با نام فایل انتخاب شده و path مشخص می‌کند که در کدام پوشه قرار داریم.
- چون در اینجا امکان حذف یک پوشه یا فایل وجود دارد، حتما نیاز است بررسی کنیم، مسیر دریافتی پوشه‌است یا فایل و سپس بر این اساس جهت حذف آن‌ها اقدام صورت گیرد.


مدیریت ایجاد یک پوشه‌ی جدید

تنظیمات قسمت create مرورگر تصاویر، مرتبط است به زمانیکه کاربر با کلیک بر روی دکمه‌ی +، درخواست ایجاد یک پوشه‌ی جدید را کرده‌است:
imageBrowser: {
    transport: {
        create: {
            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
            type: "POST"
        }
    }
},
کدهای اکشن متد متناظر با این عمل را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult CreateFolder(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
            var dirToCreate = Path.Combine(path, name);
 
            Directory.CreateDirectory(dirToCreate);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Type = KendoDirType
            });
        }
 
        protected ActionResult KendoFile(KendoFile file)
        {
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(file,
                    new JsonSerializerSettings
                    {
                        ContractResolver = new CamelCasePropertyNamesContractResolver()
                    }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
- در اینجا نیز name مساوی نام پوشه‌ی درخواستی است و path به مسیر تو در توی پوشه‌ی جاری اشاره می‌کند.
- پس از ایجاد پوشه، باید نام آن‌را با فرمت KendoFile به صورت JSON بازگشت داد. همچنین در اینجا Type را نیز باید به d (پوشه) تنظیم کرد.


مدیریت قسمت ارسال فایل و تصویر

زمانیکه کاربر بر روی دکمه‌ی upload file یا بارگذاری تصاویر در اینجا کلیک می‌کند، اطلاعات فایل آپلودی به مسیر uploadUrl ارسال می‌گردد.
imageBrowser: {
    transport: {
        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
    }
},
دو تنظیم دیگر thumbnailUrl و imageUrl، برای نمایش بند انگشتی و نمایش کامل تصویر کاربرد دارند.
در ادامه کدهای مدیریت سمت سرور قسمت آپلود این ادیتور را مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";

 
        [HttpPost]
        public ActionResult UploadFile(HttpPostedFileBase file, string path)
        {
            //تمیز سازی امنیتی
            var name = Path.GetFileName(file.FileName);
            path = GetSafeDirPath(path);
            var pathToSave = Path.Combine(path, name);
 
            file.SaveAs(pathToSave);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Size = file.ContentLength,
                Type = KendoFileType
            });
        } 
    }
}
- در اینجا path مشخص می‌کند که در کدام پوشه‌ی تو در تو قرار داریم و file نیز حاوی محتوای ارسالی به سرور است.
- پس از ذخیره سازی اطلاعات فایل، نیاز است اطلاعات فایل نهایی را با فرمت KendoFile به صورت JSON بازگشت دهیم.


ارث بری از KendoEditorFilesController جهت تکمیل قسمت مدیریت تصاویر

تا اینجا کدهایی را که ملاحظه کردید، برای هر دو قسمت ارسال تصویر و فایل کاربرد دارند. قسمت ارسال تصاویر برای تکمیل نیاز به متد دریافت تصاویر به صورت بند انگشتی نیز دارد که به صورت ذیل قابل تعریف است و چون از کلاس پایه KendoEditorFilesController ارث بری کرده‌است، این کنترلر به صورت خودکار حاوی اکشن متدهای کلاس پایه نیز خواهد بود.
using System.Web.Mvc;
 
namespace KendoUI13.Controllers
{
    public class KendoEditorImagesController : KendoEditorFilesController
    {
        public KendoEditorImagesController()
        {
            // بازنویسی مسیر پوشه‌ی فایل‌ها
            FilesFolder = "~/images";
        }
 
        [HttpGet]
        [OutputCache(Duration = 3600, VaryByParam = "path")]
        public ActionResult GetThumbnail(string path)
        {
            //todo: create thumb/ resize image
 
            path = GetSafeFileAndDirPath(path);
            return File(path, "image/png");
        }
    }
}

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


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

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

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


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

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


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


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

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

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

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

       // ...
    }

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

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

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

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


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

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

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



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-20.zip
نظرات مطالب
مدیریت کلیدهای کیبرد در جاوا اسکریپت
سوال خوبی است. قبل از پاسخ به سوال شما باید کمی درباره رویدادها در عناصر DOM توضیح داده بشه. 
در صفحات html به صورت پیش فرض، هر رویدادی که در عناصر زیردست یا فرزند رخ میدهد به ترتیب در تمام عناصر والدش انتشار می‌یابد. به این فرایند event propagation یا event bubble up میگویند.
در حالت عادی برای تغییر رفتار پیش فرض مدیریت رویدادهای عناصر DOM در مرورگرها، با استفاده از زبان جاوا اسکریپت چند روش وجود دارد.
مثلا آرگومان رویداد (همان متغیر معروف e) در مرورگرهای مدرن دو متد برای اینکار دارد:
e.preventDefault()
e.stopPropagation()
متد preventDefault همانطور که از نامش هم پیداست از اجرای رفتار پیش فرض رویداد جاری جلوگیری میکند. مثلا با قرار دادن این متد در رویداد مربوط به کلیک یک لینک، رفتار پیش فرض کلیک روی لینک (همان انتقال به آدرس تنظیم شده در پراپرتی src لینک) اجرا نمیشود.
متد stopPropagation هم همانطور که از نامش پیداست از انتشار رویداد جاری به عناصر والدش جلوگیری میکند.
راه حل دیگری که برای تغییر این رفتارهای پیش فرض وجود دارد استفاده از return flase در انتهای رویداد است. این کار تاثیری همانند استفاده از متد preventDefault دارد. تفاوت این دو روش آن است که برگشت مقدار false تنها میتواند در انتهای یک متد استفاده شود بنابراین اگر خطایی در متد مربوطه رخ دهد احتمال اجرا نشدن این خط و درنتیجه اجرای ادامه فرایند با اجرای رفتار پیش فرض رویداد مربوطه وجود دارد. اما متد preventDefault را میتوان در ابتدای متد رویداد استفاده کرد و خیال خود را راحت کرد!
نکته: دو متد اشاره شده در نسخه‌های قدیمی مرورگرها (مثلا IE 8 و قبلتر) وجود ندارد!
برای آزمایش تمامی این موارد میتوان از کد زیر که برای اینکار آماده کردم استفاده کرد:
<html>
<body>
<div onclick="document.body.appendChild(outter)" style='border:solid 1px'>
    Outer Div
    <a id="inner" href="index2.htm">index2</a>
</div>
<script>
var outter = document.createElement('div');
outter.innerText = 'outter';
var inner = document.createElement('div');
inner.innerText = 'inner';

document.getElementById('inner').onclick = function(e) {
    e = e || window.event;
    document.body.appendChild(inner);
    //e.preventDefault();
    //e.stopPropagation();
    //e.returnValue = false;
    return false;
};
</script>
</body>
</html>
البته درصورت استفاده از jQuery به دلیل اینکه این کتابخانه از نمونه ای سفارشی برای آرگومان رویدادها استفاده میکند فرایندهای موجود کمی تفاوت دارد.
اطلاعات بیشتر: ^ و ^
مطالب
Angular CLI - قسمت اول - نصب و راه اندازی
از زمان Angular 2 به بعد، تنها یک نام برای نگارش‌های جدید آن درنظر گرفته شده‌است و آن هم «Angular» است. بنابراین در اینجا منظور از Angular همان +AngularJS 2.0 است.
ایجاد و توزیع برنامه‌های جدید AngularJS به همراه تمام وابستگی‌های آن‌ها و همچنین رعایت بهترین تجربه‌های کاری آن، اندکی مشکل است. به همین جهت تیم Angular برنامه‌ای را به نام Angular CLI تدارک دیده‌است که تمام این مراحل را به سادگی هرچه تمام‌تر مدیریت می‌کند. ممکن است قالب‌های زیادی را در مورد شروع به کار با AngularJS 2.0+ در وب پیدا کنید؛ اما هیچکدام از آن‌ها تمام قابلیت‌های Angular CLI را ارائه نمی‌دهند و همواره چندین قدم عقب‌تر از تیم Angular هستند. به همین جهت در طی یک سری قصد داریم قابلیت‌های گوناگون این ابزار را بررسی کنیم.


Angular CLI چیست؟

ایجاد برنامه‌های جدید Angular لذت بخش هستند؛ اما ایجاد برنامه‌هایی که از بهترین تجربه‌های کاری توصیه شده‌ی توسط تیم Angular پیروی می‌کنند، به همراه Unit tests هستند و همچنین برای توزیع بهینه سازی شده‌اند، بسیار چالش برانگیز می‌باشند. به همین جهت برنامه‌ی خط فرمانی به نام Angular CLI برای مدیریت این مسایل توسط تیم Angular ایجاد شده‌است، تا توسعه دهندگان بیشتر وقت خود را صرف بهینه سازی کدهای خود کنند تا اینکه درگیر تدارک مسایل جانبی این فریم ورک باشند.
اگر به پروژه‌های سورس باز ارائه شده‌ی جهت شروع کار با +AngularJS 2.0 دقت کنید، تعداد بی‌شماری پروژه‌ی seed، قالب‌های آماده، کدساز و غیره را خواهید یافت. اکثر آن‌ها تفاوت‌های قابل ملاحظه‌ای را با یکدیگر داشته و در اغلب موارد بهترین تجربه‌های کاری Angular را نیز رعایت نمی‌کنند. برای مثال خبری از style guide آن و یا مباحث بهینه سازی ساخت و توزیع لحاظ شده‌ی در نگارش‌های جدید Angular، در آن‌ها نیست.
در اینجا بود که تیم Angular تصمیم گرفت تا در جهت ساماندهی به این وضعیت آشفته، برنامه‌ی Angular CLI را ایجاد کند تا برنامه نویس‌ها به همراه ابزاری باشند که بر اساس بهترین تجربه‌های کاری Angular تهیه شده‌است؛ سبب ایجاد برنامه‌هایی خواهد شد که یکدست به نظر می‌رسند و همچنین همواره آخرین تغییرات توزیع و آزمایش برنامه‌ها را نیز به همراه دارد.


پیشنیازهای نصب Angular CLI

پیش از شروع به نصب Angular CLI باید مطمئن شوید که آخرین نگارش NodeJS را نصب کرده‌اید. برای این منظور خط فرمان را گشوده و دستور ذیل را صادر کنید:
C:\>node -v
v5.10.1
در اینجا نگارش نصب شده‌ی بر روی سیستم من 5.10 است که برای کار با Angular CLI مناسب نیست و این برنامه‌ی خط فرمان، حداقل نیاز به نصب نگارش 6.9 آن‌را دارد. به همین جهت نیاز است به آدرس https://nodejs.org/en/download مراجعه کرده و آخرین نگارش node.js را دریافت و نصب کرد.

اگر این مطلب را در چند ماه بعد پس از نگارش آن مطالعه می‌کنید، به پروژه‌ی Angular CLI مراجعه کرده و قسمت Prerequisites مستندات ابتدایی آن‌را برای مشاهده‌ی آخرین نگارش NodeJS مورد نیاز آن، بررسی کنید.


نصب Angular CLI

پس از نصب پیشنیاز آن، اکنون خط فرمان را گشوده و دستور ذیل را صادر کنید:
 C:\>npm install -g @angular/cli
به این ترتیب پس از چند دقیقه، Angular CLI به صورت global و عمومی نصب خواهد شد.

پس از نصب آن، جهت اطمینان از عملیات انجام شده، دستور ذیل را در خط فرمان صادر کنید:
 C:\>npm list -g @angular/cli --depth=0
کار سوئیچ list، ارائه گزارشی از بسته‌های عمومی نصب شده‌ی با نام angular/cli@ است. depth=0 به این معنا است که نیازی به تهیه لیستی از وابستگی‌های آن نیست. برای نمونه خروجی آن می‌تواند به صورت ذیل باشد:
 C:\>npm list -g @angular/cli --depth=0
C:\Users\Vahid\AppData\Roaming\npm
`-- @angular/cli@1.0.0

و همچنین برای مشاهده‌ی نگارش CLI نصب شده، دستور ذیل را اجرا نمائید:
C:\>ng -v
    _                      _                 ____ _     ___
   / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
 / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
               |___/
@angular/cli: 1.0.0
node: 6.10.2
os: win32 x64
در اینجا ng همان Angular CLI است.


ایجاد یک برنامه‌ی جدید توسط Angular CLI

پس از نصب Angular CLI، اکنون می‌توان از آن جهت ساخت یک برنامه‌ی جدید Angular استفاده کرد. برای این منظور یک پوشه‌ی جدید را ایجاد کرده و سپس از طریق خط فرمان به آن وارد شده (نگه داشتن دکمه‌ی shift و سپس کلیک راست و انتخاب گزینه‌ی Open command window here) و دستور ذیل را صادر کنید:
> ng new ngtest --skip-install
ng به معنای اجرای Angular CLI است. پارامتر new آن سبب ایجاد یک برنامه‌ی جدید خواهد شد و پارامتر skip-install آن، کار فراخوانی خودکار npm install را لغو می‌کند. به این ترتیب می‌توان در سریعترین زمان ممکن، یک برنامه‌ی Angular را ایجاد کرد.


در اینجا ساختار یک پروژه‌ی جدید Angular را مشاهده می‌کنید.
فایل
توضیحات
 .angular-cli.json   تنظیمات cli را به همراه دارد.
 editorconfig   مربوط به تنظیمات VSCode است.
 karma.conf.js   برای انجام unit tests است. 
 package.json    وابستگی‌های npm برنامه را به همراه دارد (که در زمان نگارش این مطلب تنظیمات Angular 4 را به همراه دارد). 
 protractor.conf.js   برای اجرای آزمون‌های end to end که در اینجا e2e نام گرفته‌است، می‌باشد. 
 tsconfig.json   تنظیمات کامپایلر TypeScript را به همراه دارد. 
 tslint.json   جهت اجرای Lint و ارائه‌ی بهترین تجربه‌های کاری با TypeScript است. 

داخل پوشه‌ی src، فایل‌های اصلی پروژه قرار دارند:

- فایل index.html کار ارائه و شروع برنامه را انجام می‌دهد.
- فایل main.ts نقطه‌ی آغاز برنامه است.

با توجه به استفاده‌ی از پارامتر skip-install، هنوز وابستگی‌های فایل package.json نصب نشده‌اند. برای این منظور به پوشه‌ی اصلی پروژه وارد شده (جایی که پوشه‌ی ngtest و فایل package.json قرار دارد) و دستور npm install را صادر کنید تا وابستگی‌های برنامه نیز دریافت شوند. البته اگر از پارامتر یاد شده استفاده نمی‌شد، اینکار به صورت خودکار توسط ng new  انجام می‌گرفت.
>npm install


به این ترتیب وابستگی‌های پروژه در پوشه‌ی node_modules تشکیل خواهند شد.


به روز رسانی Angular CLI

روش به روز رسانی AngularCLI شامل این مراحل است:
الف) به روز رسانی بسته‌ی عمومی نصب شده‌ی آن
npm uninstall -g @angular/cli
npm cache clean
npm install -g @angular/cli@latest
ابتدا باید نگارش موجود عزل شود. سپس cache قدیمی مربوط به npm نیز باید پاک شود و پس از آن نیاز است مجددا آخرین نگارش cli نصب گردد.

ب) به روز رسانی یک برنامه‌ی محلی
در ادامه به پوشه‌ی برنامه‌ی خود وارد شده و دستورات ذیل را اجرا کنید:
rm rmdir /S/Q node_modules dist
npm install --save-dev @angular/cli@latest
npm install
این دستورات ابتدا پوشه‌های node_modules و همچنین dist قبلی را پاک می‌کنند. دستور بعدی، کار به روز رسانی وابستگی‌های package.json را انجام می‌دهد و در آخر دستور npm install، تغییرات فایل package.json را دریافت و نصب می‌کند.

مورد «الف» را به ازای هر نگارش جدید CLI، تنها یکبار باید انجام داد. اما مورد «ب» به ازای هر پروژه‌ی موجود باید یکبار انجام شود (که سریعترین روش به روز رسانی وابستگی‌های یک برنامه، به آخرین نگارش Angular است).
نظرات مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
با سلام
من یک فرم دارم که در اون کاربر باید دو عکس وارد کنید به صورت زیر
 <div>
        <div style="margin: 2px 4px !important">
            <div>
                عکس 1
                <input type="file" name="imageSrc" id="imageSrc" />
                @*@Html.Kendo().Upload().Name("imageSrc").Multiple(false)*@
                @Html.ValidationMessageFor(model => model.Image)
            </div>
        </div>

    </div>
    <div>
        <div style="margin: 2px 4px !important">
            <div>
                عکس 2
                <input type="file" name="imageSrc2" id="imageSrc2" />
                @*@Html.Kendo().Upload().Name("imageSrc2").Multiple(false)*@
                @Html.ValidationMessageFor(model => model.Image)
            </div>
        </div>

آیا می‌تونم این دو فایل رو با هم آپلود کنم؟ چون در مثال شما  fileElementId: 'Image1' ,  فقط نام یک کنترل را می‌گیرد.
ممنون
مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت هفتم - بررسی مفاهیم Mobx
MobX از 4 مفهوم اصلی تشکیل می‌شود:

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

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

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

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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

   componentWillUnmount() {
     this.stopListening();
   }

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

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

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

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

const itemStore = new ItemStore();

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


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

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

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

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

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

class Count {
  value = 0;

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

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

decorate(Count, { value: observable });

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

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

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


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

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

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

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

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-with-mobx-part2.zip