مطالب
راهنمای تغییر بخش احراز هویت و اعتبارسنجی کاربران سیستم مدیریت محتوای IRIS به ASP.NET Identity – بخش سوم
تغییر الگوریتم پیش فرض هش کردن کلمه‌های عبور ASP.NET Identity

کلمه‌های عبور کاربران فعلی سیستم با الگوریتمی متفاوت از الگوریتم مورد استفاده Identity هش شده‌اند. برای اینکه کاربرانی که قبلا ثبت نام کرده بودند بتوانند با کلمه‌های عبور خود وارد سایت شوند، باید الگوریتم هش کردن Identity را با الگوریتم فعلی مورد استفاده Iris جایگزین کرد.

برای تغییر روش هش کردن کلمات عبور در Identity باید اینترفیس IPasswordHasher را پیاده سازی کنید:
    public class IrisPasswordHasher : IPasswordHasher
    {
        public string HashPassword(string password)
        {
            return Utilities.Security.Encryption.EncryptingPassword(password);
        }

        public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            return Utilities.Security.Encryption.VerifyPassword(providedPassword, hashedPassword) ?
                                                                PasswordVerificationResult.Success :
                                                                PasswordVerificationResult.Failed;
        }
    }

  سپس باید وارد کلاس ApplicationUserManager شده و در سازنده‌ی آن اینترفیس IPasswordHasher را به عنوان وابستگی تعریف کنید:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store,
            IUnitOfWork uow,
            IIdentity identity,
            IApplicationRoleManager roleManager,
            IDataProtectionProvider dataProtectionProvider,
            IIdentityMessageService smsService,
            IIdentityMessageService emailService, IPasswordHasher passwordHasher)
            : base(store)
        {
            _store = store;
            _uow = uow;
            _identity = identity;
            _users = _uow.Set<ApplicationUser>();
            _roleManager = roleManager;
            _dataProtectionProvider = dataProtectionProvider;
            this.SmsService = smsService;
            this.EmailService = emailService;
            PasswordHasher = passwordHasher;
            createApplicationUserManager();
        }

برای اینکه کلاس IrisPasswordHasher را به عنوان نمونه درخواستی IPasswordHasher معرفی کنیم، باید در تنظیمات StructureMap کد زیر را نیز اضافه کنید:
x.For<IPasswordHasher>().Use<IrisPasswordHasher>();

پیاده سازی اکشن متد ثبت نام کاربر با استفاده از Identity

در کنترلر UserController، اکشن متد Register را به شکل زیر بازنویسی کنید:
        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public virtual async Task<ActionResult> Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    CreatedDate = DateAndTime.GetDateTime(),
                    Email = model.Email,
                    IP = Request.ServerVariables["REMOTE_ADDR"],
                    IsBaned = false,
                    UserName = model.UserName,
                    UserMetaData = new UserMetaData(),
                    LastLoginDate = DateAndTime.GetDateTime()
                };

                var result = await _userManager.CreateAsync(user, model.Password);

                if (result.Succeeded)
                {
                    var addToRoleResult = await _userManager.AddToRoleAsync(user.Id, "user");
                    if (addToRoleResult.Succeeded)
                    {
                        var code = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id);
                        var callbackUrl = Url.Action("ConfirmEmail", "User",
                            new { userId = user.Id, code }, protocol: Request.Url.Scheme);

                        _emailService.SendAccountConfirmationEmail(user.Email, callbackUrl);

                        return Json(new { result = "success" });
                    }

                    addErrors(addToRoleResult);
                }

                addErrors(result);
            }

            return PartialView(MVC.User.Views._Register, model);
        }
نکته: در اینجا برای ارسال لینک فعال سازی حساب کاربری، از کلاس EmailService خود سیستم IRIS استفاده شده است؛ نه EmailService مربوط به ASP.NET Identity. همچنین در ادامه نیز از EmailService مربوط به خود سیستم Iris استفاده شده است.

برای این کار متد زیر را به کلاس EmailService  اضافه کنید: 
        public SendingMailResult SendAccountConfirmationEmail(string email, string link)
        {
            var model = new ConfirmEmailModel()
            {
                ActivationLink = link
            };

            var htmlText = _viewConvertor.RenderRazorViewToString(MVC.EmailTemplates.Views._ConfirmEmail, model);

            var result = Send(new MailDocument
            {
                Body = htmlText,
                Subject = "تایید حساب کاربری",
                ToEmail = email
            });

            return result;
        }
همچنین قالب ایمیل تایید حساب کاربری را در مسیر Views/EmailTemplates/_ConfirmEmail.cshtml با محتویات زیر ایجاد کنید:
@model Iris.Model.EmailModel.ConfirmEmailModel

<div style="direction: rtl; -ms-word-wrap: break-word; word-wrap: break-word;">
    <p>با سلام</p>
    <p>برای فعال سازی حساب کاربری خود لطفا بر روی لینک زیر کلیک کنید:</p>
    <p>@Model.ActivationLink</p>
    <div style=" color: #808080;">
        <p>با تشکر</p>
        <p>@Model.SiteTitle</p>
        <p>@Model.SiteDescription</p>
        <p><span style="direction: ltr !important; unicode-bidi: embed;">@Html.ConvertToPersianDateTime(DateTime.Now, "s,H")</span></p>
    </div>
</div>

اصلاح پیام موفقیت آمیز بودن ثبت نام  کاربر جدید


سیستم IRIS از ارسال ایمیل تایید حساب کاربری استفاده نمی‌کند و به محض اینکه عملیات ثبت نام تکمیل می‌شد، صفحه رفرش می‌شود. اما در سیستم Identity یک ایمیل حاوی لینک فعال سازی حساب کاربری به او ارسال می‌شود.
برای اصلاح پیغام پس از ثبت نام، باید به فایل myscript.js درون پوشه‌ی Scripts مراجعه کرده و رویداد onSuccess شیء RegisterUser را به شکل زیراصلاح کنید: 
RegisterUser.Form.onSuccess = function (data) {
    if (data.result == "success") {
        var message = '<div id="alert"><button type="button" data-dismiss="alert">×</button>ایمیلی حاوی لینک فعال سازی، به ایمیل شما ارسال شد؛ لطفا به ایمیل خود مراجعه کرده و بر روی لینک فعال سازی کلیک کنید.</div>';
        $('#registerResult').html(message);
    }
    else {
        $('#logOnModal').html(data);
    }
};
برای تایید ایمیل کاربری که ثبت نام کرده است نیز اکشن متد زیر را به کلاس UserController اضافه کنید:
        [AllowAnonymous]
        public virtual async Task<ActionResult> ConfirmEmail(int? userId, string code)
        {
            if (userId == null || code == null)
            {
                return View("Error");
            }
            var result = await _userManager.ConfirmEmailAsync(userId.Value, code);
            return View(result.Succeeded ? "ConfirmEmail" : "Error");
        }
این اکشن متد نیز احتیاج به View دارد؛ پس view متناظر آن را با محتویات زیر اضافه کنید:
@{
    ViewBag.Title = "حساب کاربری شما تایید شد";
}
<h2>@ViewBag.Title.</h2>
<div>
    <p>
        با تشکر از شما، حساب کاربری شما تایید شد.
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

اصلاح اکشن متد ورود به سایت 

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async virtual Task<ActionResult> LogOn(LogOnModel model, string returnUrl)
        {
            if (!ModelState.IsValid)
            {
                if (Request.IsAjaxRequest())
                    return PartialView(MVC.User.Views._LogOn, model);
                return View(model);
            }


            const string emailRegPattern =
                @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$";

            string ip = Request.ServerVariables["REMOTE_ADDR"];

            SignInStatus result = SignInStatus.Failure;

            if (Regex.IsMatch(model.Identity, emailRegPattern))
            {

                var user = await _userManager.FindByEmailAsync(model.Identity);

                if (user != null)
                {
                    result = await _signInManager.PasswordSignInAsync
                   (user.UserName,
                   model.Password, model.RememberMe, shouldLockout: true);
                }
            }
            else
            {
                result = await _signInManager.PasswordSignInAsync(model.Identity, model.Password, model.RememberMe, shouldLockout: true);
            }


            switch (result)
            {
                case SignInStatus.Success:
                    if (Request.IsAjaxRequest())
                        return JavaScript(IsValidReturnUrl(returnUrl)
                            ? string.Format("window.location ='{0}';", returnUrl)
                            : "window.location.reload();");
                    return redirectToLocal(returnUrl);

                case SignInStatus.LockedOut:
                    ModelState.AddModelError("",
                        string.Format("حساب شما قفل شد، لطفا بعد از {0} دقیقه دوباره امتحان کنید.",
                            _userManager.DefaultAccountLockoutTimeSpan.Minutes));
                    break;
                case SignInStatus.Failure:
                    ModelState.AddModelError("", "نام کاربری یا کلمه عبور اشتباه است.");
                    break;
                default:
                    ModelState.AddModelError("", "در ورود شما خطایی رخ داده است.");
                    break;
            }


            if (Request.IsAjaxRequest())
                return PartialView(MVC.User.Views._LogOn, model);
            return View(model);
        }


اصلاح اکشن متد خروج کاربر از سایت

        [HttpPost]
        [ValidateAntiForgeryToken]
        [Authorize]
        public virtual ActionResult LogOut()
        {
            _authenticationManager.SignOut();

            if (Request.IsAjaxRequest())
                return Json(new { result = "true" });

            return RedirectToAction(MVC.User.ActionNames.LogOn, MVC.User.Name);
        }

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

مکانیزم سیستم IRIS برای ریست کردن کلمه‌ی عبور به هنگام فراموشی آن، ساخت GUID و ذخیره‌ی آن در دیتابیس است. سیستم Identity  با استفاده از یک توکن رمز نگاری شده و بدون استفاده از دیتابیس، این کار را انجام می‌دهد و با استفاده از قابلیت‌های تو کار سیستم Identity، تمهیدات امنیتی بهتری را نسبت به سیستم کنونی در نظر گرفته است.

برای این کار کدهای کنترلر ForgottenPasswordController را به شکل زیر ویرایش کنید:
using System.Threading.Tasks;
using System.Web.Mvc;
using CaptchaMvc.Attributes;
using Iris.Model;
using Iris.Servicelayer.Interfaces;
using Iris.Web.Email;
using Microsoft.AspNet.Identity;

namespace Iris.Web.Controllers
{
    public partial class ForgottenPasswordController : Controller
    {
        private readonly IEmailService _emailService;
        private readonly IApplicationUserManager _userManager;

        public ForgottenPasswordController(IEmailService emailService, IApplicationUserManager applicationUserManager)
        {
            _emailService = emailService;
            _userManager = applicationUserManager;
        }

        [HttpGet]
        public virtual ActionResult Index()
        {
            return PartialView(MVC.ForgottenPassword.Views._Index);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")]
        public async virtual Task<ActionResult> Index(ForgottenPasswordModel model)
        {
            if (!ModelState.IsValid)
            {
                return PartialView(MVC.ForgottenPassword.Views._Index, model);
            }

            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.Id)))
            {
                // Don't reveal that the user does not exist or is not confirmed
                return Json(new
                {
                    result = "false",
                    message = "این ایمیل در سیستم ثبت نشده است"
                });
            }

            var code = await _userManager.GeneratePasswordResetTokenAsync(user.Id);

            _emailService.SendResetPasswordConfirmationEmail(user.UserName, user.Email, code);

            return Json(new
            {
                result = "true",
                message = "ایمیلی برای تایید بازنشانی کلمه عبور برای شما ارسال شد.اعتبارایمیل ارسالی 3 ساعت است."
            });
        }

        [AllowAnonymous]
        public virtual ActionResult ResetPassword(string code)
        {
            return code == null ? View("Error") : View();
        }


        [AllowAnonymous]
        public virtual ActionResult ResetPasswordConfirmation()
        {
            return View();
        }

        //
        // POST: /Account/ResetPassword
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public virtual async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return RedirectToAction("Error");
            }
            var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction("ResetPasswordConfirmation", "ForgottenPassword");
            }
            addErrors(result);
            return View();
        }

        private void addErrors(IdentityResult result)
        {
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }

    }
}

همچنین برای اکشن متدهای اضافه شده، View‌های زیر را نیز باید اضافه کنید:

- View  با نام  ResetPasswordConfirmation.cshtml را اضافه کنید.
@{
    ViewBag.Title = "کلمه عبور شما تغییر کرد";
}

<hgroup>
    <h1>@ViewBag.Title.</h1>
</hgroup>
<div>
    <p>
        کلمه عبور شما با موفقیت تغییر کرد
    </p>
    <p>
        @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" })
    </p>
</div>

- View با نام ResetPassword.cshtml
@model Iris.Model.ResetPasswordViewModel
@{
    ViewBag.Title = "ریست کردن کلمه عبور";
}
<h2>@ViewBag.Title.</h2>
@using (Html.BeginForm("ResetPassword", "ForgottenPassword", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>ریست کردن کلمه عبور</h4>
    <hr />
    @Html.ValidationSummary("", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Code)
    <div>
        @Html.LabelFor(m => m.Email, "ایمیل", new { @class = "control-label" })
        <div>
            @Html.TextBoxFor(m => m.Email)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.Password, "کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.Password)
        </div>
    </div>
    <div>
        @Html.LabelFor(m => m.ConfirmPassword, "تکرار کلمه عبور", new { @class = "control-label" })
        <div>
            @Html.PasswordFor(m => m.ConfirmPassword)
        </div>
    </div>
    <div>
        <div>
            <input type="submit" value="تغییر کلمه عبور" />
        </div>
    </div>
}

همچنین این View و Controller متناظر آن، احتیاج به ViewModel زیر دارند که آن را به پروژه‌ی Iris.Models اضافه کنید.
using System.ComponentModel.DataAnnotations;
namespace Iris.Model
{
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        [Display(Name = "ایمیل")]
        public string Email { get; set; }

        [Required]
        [StringLength(100, ErrorMessage = "کلمه عبور باید حداقل 6 حرف باشد", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "کلمه عبور")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "تکرار کلمه عبور")]
        [Compare("Password", ErrorMessage = "کلمه عبور و تکرارش یکسان نیستند")]
        public string ConfirmPassword { get; set; }

        public string Code { get; set; }
    }
}

حذف سیستم قدیمی احراز هویت

برای حذف کامل سیستم احراز هویت IRIS، وارد فایل Global.asax.cs شده و سپس از متد Application_AuthenticateRequest کدهای زیر را حذف کنید:

var principalService = ObjectFactory.GetInstance<IPrincipalService>();
var formsAuthenticationService = ObjectFactory.GetInstance<IFormsAuthenticationService>();
context.User = principalService.GetCurrent()

فارسی کردن خطاهای ASP.NET Identity

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

راه اول این است که از این پروژه استفاده کرد و کلاس‌های زیر را به پروژه اضافه کنید:

    public class CustomUserValidator<TUser, TKey> : IIdentityValidator<ApplicationUser>
        where TUser : class, IUser<int>
        where TKey : IEquatable<int>
    {

        public bool AllowOnlyAlphanumericUserNames { get; set; }
        public bool RequireUniqueEmail { get; set; }

        private ApplicationUserManager Manager { get; set; }
        public CustomUserValidator(ApplicationUserManager manager)
        {
            if (manager == null)
                throw new ArgumentNullException("manager");
            AllowOnlyAlphanumericUserNames = true;
            Manager = manager;
        }
        public virtual async Task<IdentityResult> ValidateAsync(ApplicationUser item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var errors = new List<string>();
            await ValidateUserName(item, errors);
            if (RequireUniqueEmail)
                await ValidateEmailAsync(item, errors);
            return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }

        private async Task ValidateUserName(ApplicationUser user, ICollection<string> errors)
        {
            if (string.IsNullOrWhiteSpace(user.UserName))
                errors.Add("نام کاربری نباید خالی باشد");
            else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, "^[A-Za-z0-9@_\\.]+$"))
            {
                errors.Add("برای نام کاربری فقط از کاراکتر‌های مجاز استفاده کنید ");
            }
            else
            {
                var owner = await Manager.FindByNameAsync(user.UserName);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این نام کاربری قبلا ثبت شده است");
            }
        }

        private async Task ValidateEmailAsync(ApplicationUser user, ICollection<string> errors)
        {
            var email = await Manager.GetEmailStore().GetEmailAsync(user).WithCurrentCulture();
            if (string.IsNullOrWhiteSpace(email))
            {
                errors.Add("وارد کردن ایمیل ضروریست");
            }
            else
            {
                try
                {
                    var m = new MailAddress(email);

                }
                catch (FormatException)
                {
                    errors.Add("ایمیل را به شکل صحیح وارد کنید");
                    return;
                }
                var owner = await Manager.FindByEmailAsync(email);
                if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id))
                    errors.Add("این ایمیل قبلا ثبت شده است");
            }
        }
    }

    public class CustomPasswordValidator : IIdentityValidator<string>
    {
        #region Properties
        public int RequiredLength { get; set; }
        public bool RequireNonLetterOrDigit { get; set; }
        public bool RequireLowercase { get; set; }
        public bool RequireUppercase { get; set; }
        public bool RequireDigit { get; set; }
        #endregion

        #region IIdentityValidator
        public virtual Task<IdentityResult> ValidateAsync(string item)
        {
            if (item == null)
                throw new ArgumentNullException("item");
            var list = new List<string>();

            if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength)
                list.Add(string.Format("کلمه عبور نباید کمتر از 6 کاراکتر باشد"));

            if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit))
                list.Add("برای امنیت بیشتر از حداقل از یک کارکتر غیر عددی و غیر حرف  برای کلمه عبور استفاده کنید");

            if (RequireDigit && item.All(c => !IsDigit(c)))
                list.Add("برای امنیت بیشتر از اعداد هم در کلمه عبور استفاده کنید");
            if (RequireLowercase && item.All(c => !IsLower(c)))
                list.Add("از حروف کوچک نیز برای کلمه عبور استفاده کنید");
            if (RequireUppercase && item.All(c => !IsUpper(c)))
                list.Add("از حروف بزرک نیز برای کلمه عبور استفاده کنید");
            return Task.FromResult(list.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(string.Join(" ", list)));
        }

        #endregion

        #region PrivateMethods
        public virtual bool IsDigit(char c)
        {
            if (c >= 48)
                return c <= 57;
            return false;
        }

        public virtual bool IsLower(char c)
        {
            if (c >= 97)
                return c <= 122;
            return false;
        }


        public virtual bool IsUpper(char c)
        {
            if (c >= 65)
                return c <= 90;
            return false;
        }

        public virtual bool IsLetterOrDigit(char c)
        {
            if (!IsUpper(c) && !IsLower(c))
                return IsDigit(c);
            return true;
        }
        #endregion

    }

سپس باید کلاس‌های فوق را به Identity معرفی کنید تا از این کلاس‌های سفارشی شده به جای کلاس‌های پیش فرض خودش استفاده کند. برای این کار وارد کلاس ApplicationUserManager شده و درون متد createApplicationUserManager کدهای زیر را اضافه کنید: 
            UserValidator = new CustomUserValidator< ApplicationUser, int>(this)
            {
                AllowOnlyAlphanumericUserNames = false,
                RequireUniqueEmail = true
            };

            PasswordValidator = new CustomPasswordValidator
            {
                RequiredLength = 6,
                RequireNonLetterOrDigit = false,
                RequireDigit = false,
                RequireLowercase = false,
                RequireUppercase = false
            };
روش دیگر مراجعه به سورس ASP.NET Identity است. با مراجعه به مخزن کد آن، فایل Resources.resx آن را که حاوی متن‌های خطا به زبان انگلیسی است، درون پروژه‌ی خود کپی کنید. همچین کلاس‌های UserValidator و PasswordValidator را نیز درون پروژه کپی کنید تا این کلاس‌ها از فایل Resource موجود در پروژه‌ی خودتان استفاده کنند. در نهایت همانند روش قبلی درون متد createApplicationUserManager کلاس ApplicationUserManager، کلاس‌های UserValidator و PasswordValidator را به Identity معرفی کنید.


ایجاد SecurityStamp برای کاربران فعلی سایت

سیستم Identity برای لحاظ کردن یک سری موارد امنیتی، به ازای هر کاربر، فیلدی را به نام SecurityStamp درون دیتابیس ذخیره می‌کند و برای این که این سیستم عملکرد صحیحی داشته باشد، باید این مقدار را برای کاربران فعلی سایت ایجاد کرد تا کاربران فعلی بتوانند از امکانات Identity نظیر فراموشی کلمه عبور، ورود به سیستم و ... استفاده کنند.
برای این کار Identity، متدی به نام UpdateSecurityStamp را در اختیار قرار می‌دهد تا با استفاده از آن بتوان مقدار فیلد SecurityStamp را به روز رسانی کرد.
معمولا برای انجام این کارها می‌توانید یک کنترلر تعریف کنید و درون اکشن متد آن کلیه‌ی کاربران را واکشی کرده و سپس متد UpdateSecurityStamp را بر روی آن‌ها فراخوانی کنید.
        public virtual async Task<ActionResult> UpdateAllUsersSecurityStamp()
        {
            foreach (var user in await _userManager.GetAllUsersAsync())
            {
                await _userManager.UpdateSecurityStampAsync(user.Id);
            }
            return Content("ok");
        }
البته این روش برای تعداد زیاد کاربران کمی زمان بر است.


انتقال نقش‌های کاربران به جدول جدید و برقراری رابطه بین آن‌ها

در سیستم Iris رابطه‌ی بین کاربران و نقش‌ها یک به چند بود. در سیستم Identity این رابطه چند به چند است و من به عنوان یک حرکت خوب و رو به جلو، رابطه‌ی چند به چند را در سیستم جدید انتخاب کردم. اکنون با استفاده از دستورات زیر به راحتی می‌توان نقش‌های فعلی و رابطه‌ی بین آن‌ها را به جداول جدیدشان منتقل کرد:
        public virtual async Task<ActionResult> CopyRoleToNewTable()
        {
            var dbContext = new IrisDbContext();

            foreach (var role in await dbContext.Roles.ToListAsync())
            {
                await _roleManager.CreateAsync(new CustomRole(role.Name)
                {
                    Description = role.Description
                });
            }

            var users = await dbContext.Users.Include(u => u.Role).ToListAsync();

            foreach (var user in users)
            {
                await _userManager.AddToRoleAsync(user.Id, user.Role.Name);
            }
            return Content("ok");
        }
البته اجرای این کد نیز برای تعداد زیادی کاربر، زمانبر است؛ ولی روشی مطمئن و دقیق است.
مطالب دوره‌ها
مثال - نمایش بلادرنگ تعداد کاربران آنلاین توسط SignalR
راه حل‌های زیادی برای محاسبه و نمایش تعداد کاربران آنلاین یک برنامه وب وجود دارند و عموما مبتنی بر کار با متغیرهای سشن یا Application و امثال آن هستند. این روش‌ها عموما دقیق نبوده و خصوصا قسمت قطع اتصال کاربر را نمی‌توانند دقیقا تشخیص دهند. به همین جهت نیاز به یک تایمر دارند که مثلا اگر در 5 دقیقه قبل، کاربری درخواست مشاهده آدرسی را به سرور ارسال نکرده بود، از لیست کاربران آنلاین حذف شود.
در ادامه بجای این روش‌ها، از SignalR برای محاسبه تعداد کاربران آنلاین و همچنین به روز رسانی بلادرنگ این عدد در سمت کاربر، استفاده خواهیم کرد.

تشخیص اتصال و قطع اتصال کاربران در SignalR

زیر ساخت‌های کلاس Hub موجود در SignalR، دارای متدهای ردیابی اتصال (OnConnected)، قطع اتصال (OnDisconnected) و یا برقراری مجدد اتصال کاربران (OnReconnected) هستند. با بازنویسی این متدها می‌توان به تخمین بسیار دقیقی از تعداد کاربران آنلاین یک سایت رسید.


پیشنیازهای بحث
پیشنیازهای این بحث با مطلب «مثال - نمایش درصد پیشرفت عملیات توسط SignalR» یکی است. برای مثال نحوه دریافت وابستگی‌ها، تنظیمات فایل global.asax و افزودن اسکریپت‌ها، تفاوتی با مثال یاد شده ندارند.


تعریف هاب کاربران آنلاین برنامه

using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;

namespace SignalR05.Common
{
    public class OnlineUsersHub : Hub
    {
        public static readonly ConcurrentDictionary<string, string> OnlineUsers = new ConcurrentDictionary<string, string>();

        public void UpdateUsersOnlineCount()
        {
            // آی پی معرف یک کاربر است
            // اما کانکشن آی دی معرف یک برگه جدید در مرورگر او است
            // هر کاربر می‌تواند چندین برگه را به یک سایت گشوده یا ببندد
            var ipsCount = OnlineUsers.Select(x => x.Value).Distinct().Count();
            this.Clients.All.updateUsersOnlineCount(ipsCount);
        }

        /// <summary>
        /// اگر کاربران اعتبار سنجی شده‌اند بهتر است از
        /// this.Context.User.Identity.Name
        /// بجای آی پی استفاده شود
        /// </summary>        
        protected string GetUserIpAddress()
        {
            object environment;
            if (!Context.Request.Items.TryGetValue("owin.environment", out environment))
                return null;

            object serverRemoteIpAddress;
            if (!((IDictionary<string, object>)environment).TryGetValue("server.RemoteIpAddress", out serverRemoteIpAddress))
                return null;

            return serverRemoteIpAddress.ToString();
        }

        public override Task OnConnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnConnected();
        }

        public override Task OnReconnected()
        {
            var ip = GetUserIpAddress();
            OnlineUsers.TryAdd(this.Context.ConnectionId, ip);
            UpdateUsersOnlineCount();

            return base.OnReconnected();
        }

        public override Task OnDisconnected()
        {
            // در این حالت ممکن است مرورگر کاملا بسته شده باشد
            // یا حتی صرفا یک برگه مرورگر از چندین برگه متصل به سایت بسته شده باشند
            string ip;
            OnlineUsers.TryRemove(this.Context.ConnectionId, out ip);
            UpdateUsersOnlineCount();

            return base.OnDisconnected();
        }
    }
}
کدهای کامل هاب شمارش کاربران آنلاین را در اینجا ملاحظه می‌کنید؛ به همراه نکته‌ی نحوه‌ی دریافت IP کاربر متصل شده به سایت، در یک هاب. کار افزودن یا حذف این کاربران به ConcurrentDictionary تعریف شده، در روال‌های بازنویسی شده اتصال، قطع اتصال و اتصال مجدد یک کاربر، انجام شده است.
در اینجا، هم به IP کاربر و هم به ConnectionId او نیاز است. از این جهت که هر ConnectionId، معرف یک برگه جدید باز شده در مرورگر کاربر است. اگر صرفا IPها را پردازش کنیم، با بسته شدن یکی از چندین برگه مرورگر او که اکنون به سایت متصل هستند، آمار او را از دست خواهیم داد. این کاربر هنوز چندین برگه باز دیگر را دارد که با سایت در ارتباط هستند، اما چون IP او را از لیست حذف کرده‌ایم (در نتیجه بسته شدن یکی از برگه‌ها)، آمار کلی شخص را نیز از دست خواهیم داد. بنابراین هر دوی IP و ConnectionIdها باید پردازش شوند.
اگر برنامه شما دارای اعتبارسنجی است (یک صفحه لاگین دارد)، بهتر است بجای IP از this.Context.User.Identity.Name استفاده کنید.


کدهای سمت کلاینت نمایش آمار کاربران

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script>
    <script type="text/javascript" src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>
</head>
<body>
    <form id="form1" runat="server">
    online users count: <span id="usersCount"></span>
    </form>
    <script type="text/javascript">
        $(function () {
            $.connection.hub.logging = true;
            var onlineUsersHub = $.connection.onlineUsersHub;
            onlineUsersHub.client.updateUsersOnlineCount = function (count) {
                $('#usersCount').text(count);
            };
            $.connection.hub.start();
        });
    </script>
</body>
</html>
با توجه به اینکه در هاب تعریف شده، متد پویای updateUsersOnlineCount، آمار تعداد کاربران متصل را (تعداد آی پی‌های منحصربفرد متصل را) به کلاینت‌ها ارسال می‌کند، بنابراین در سمت کلاینت نیز با تعریف callback ایی به همین نام، می‌توان این آمار دریافتی را به کاربران سایت نمایش داد. آماری که به صورت خودکار با کم و زیاد شدن کاربران به روز شده و نیازی نیست کاربر به صورت دستی، صفحه را به روز کند.


کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت کنید:
SignalR05.zip
 
اشتراک‌ها
آزمون اطلاعات عمومی JavaScript

JS Is Weird
JavaScript is a great programming language, but thanks to the fact that its initial release was built in only ten days back in 1995, coupled with the fact that JS is backward-compatible, it's also a bit weird. It doesn't always behave the way you might think. In this quiz, you'll be shown 25 quirky expressions and will have to guess the output. Even if you're a JS developer, most of this syntax is probably, and hopefully, not something you use in your daily life. 

آزمون اطلاعات عمومی JavaScript
اشتراک‌ها
C# در مرورگر با Blazor

 Blazor is the new Microsoft experimental framework that brings C# into any browser without a plug-in. It holds the promise of modern single-page applications, combined with the ability to use C# and its vast base-class library. Blazor takes C# development to a new level. It’s the final piece necessary to make the language a full-stack development tool. It will have all the power of the popular JavaScript frameworks, but based on the familiar languages, APIs and tooling of the Microsoft .NET Framework. 

C# در مرورگر با Blazor
اشتراک‌ها
معرفی Fit.js
تغییر ابعاد یک شیء برای قرارگرفتن درون شیءایی دیگر
معرفی Fit.js
اشتراک‌ها
راهنمای نامگذاری متغیرها در JavaScript

Variable Naming Best Practices in JavaScript

Like any other programming language, JavaScript relies heavily on well-structured and understandable code. One of the fundamental building blocks of clean JavaScript code is effective variable naming.

By adhering to certain best practices, you can significantly enhance the readability and maintainability of your JavaScript projects. Let’s dive into 12 sets of JavaScript variable naming guidelines.

راهنمای نامگذاری متغیرها در JavaScript
مطالب
آپلود فایل‌ها توسط برنامه‌های React به یک سرور ASP.NET Core به همراه نمایش درصد پیشرفت
قصد داریم اطلاعات یک فرم React را به همراه دو فایل الصاقی به آن، به سمت یک سرور ASP.NET Core ارسال کنیم؛ بطوریکه درصد پیشرفت ارسال فایل‌ها، زمان سپری شده، زمان باقی مانده و سرعت آپلود نیز گزارش داده شوند:



پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax »

- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
 dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کرده‌ایم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف و پوشه‌ی خالی wwwroot را نیز به آن اضافه می‌کنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود. البته ما برنامه را به صورت یکپارچه بر روی پورت 5001 وب سرور ASP.NET Core، مرور می‌کنیم.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios react-toastify
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای نمایش پیام‌های برنامه از کامپوننت react-toastify استفاده می‌کنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آن‌را اضافه کنیم:
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
همچنین نیاز است ToastContainer را به ابتدای متد render آن نیز اضافه کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
- برای ارسال فایل‌ها به سمت سرور از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


ایجاد کامپوننت React فرم ارسال فایل‌ها به سمت سرور

پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
import React, { useState } from "react";
import axios from "axios";
import { toast } from "react-toastify";

export default function UploadFileSimple() {

  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();


  return (
    <form>
      <fieldset className="form-group">
        <legend>Support Form</legend>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="description">
            Description
          </label>
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file1">
            File 1
          </label>
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file2">
            File 2
          </label>
          <input
            type="file"
            className="form-control"
            name="file2"
            onChange={event => setSelectedFile2(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <button
            className="btn btn-primary"
            type="submit"
          >
            Submit
          </button>
        </div>
      </fieldset>
    </form>
  );
}
کاری که تا این مرحله انجام شده، بازگشت UI فرم برنامه توسط یک functional component است.
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شده‌اند.
- مرحله‌ی بعد، دسترسی به فایل‌های انتخابی کاربر و همچنین مقدار توضیحات وارد شده‌است. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کرده‌ایم:
  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();
با React Hooks، بجای تعریف یک state، به صورت خاصیت، آن‌را صرفا use می‌کنیم و یا همان useState، که یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ مانند انتساب یک رشته‌ی خالی به description. سپس اولین خروجی متد useState که داخل یک آرایه مشخص شده‌است، همان متغیر description است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر description به نام setDescription به عنوان دومین خروجی متد useState، تعریف می‌کنیم. بنابراین متد useState، یک initialState را دریافت می‌کند و سپس یک مقدار را به همراه یک متد، جهت تغییر state آن، بازگشت می‌دهد. همین کار را برای دو فیلد دیگر نیز تکرار کرده‌ایم. بنابراین selectedFile1، فایلی است که توسط متد setSelectedFile1 تنظیم خواهد شد و این تنظیم، سبب رندر مجدد UI نیز خواهد گردید.
- پس از طراحی state این فرم، مرحله‌ی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، می‌توان آن‌را به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخ‌داد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
در مورد فیلدهای دریافت فایل‌ها، روش انجام اینکار به صورت زیر است:
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
چون المان‌های دریافت فایل می‌توانند بیش از یک فایل را نیز دریافت کنند (اگر ویژگی multiple، به تعریف تگ آن‌ها اضافه شود)، به همین جهت خاصیت files بر روی آن‌ها قابل دسترسی شده‌است. اما چون در اینجا ویژگی multiple ذکر نشده‌است، بنابراین تنها یک فایل توسط آن‌ها قابل دریافت است و به همین جهت دسترسی به اولین فایل و یا files[0] را در اینجا مشاهده می‌کنید. بنابراین با فراخوانی متد setSelectedFile1، اکنون متغیر حالت selectedFile1، مقدار دهی شده و قابل استفاده است.


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

در فرم‌های معمولی، عموما داده‌ها به صورت یک شیء JSON به سمت سرور ارسال می‌شوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرم‌هایی که به همراه المان‌های دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه می‌شود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا می‌کند:
<form enctype="multipart/form-data" action="/upload" method="post">
   <input id="file-input" type="file" />
</form>
اما اگر قرار باشد همین فرم را توسط جاوا اسکریپت به سمت سرور ارسال کنیم، روش کار به صورت زیر است:
let file = document.getElementById("file-input").files[0];
let formData = new FormData();
 
formData.append("file", file);
fetch('/upload/image', {method: "POST", body: formData});
ابتدا به خاصیت files و اولین فایل آن دسترسی پیدا کرده و سپس شیء استاندارد FormData را بر اساس آن و تمام فیلدهای فرم تشکیل می‌دهیم. FormData ساختاری شبیه به یک دیکشنری را دارد و از کلیدهایی که متناظر با Id المان‌های فرم و مقادیری متناظر با مقادیر آن المان‌ها هستند، تشکیل می‌شود که توسط متد append آن، به این دیکشنری اضافه خواهند شد. در آخر هم شیء formData را به سمت سرور ارسال می‌کنیم.
در یک برنامه‌ی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام داده‌ایم. در مرحله‌ی بعد، شیء FormData را تشکیل خواهیم داد:
  // ...

export default function UploadFileSimple() {
  // ...

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);


      toast.success("Form has been submitted successfully!");

      setDescription("");
  };

  return (
    <form onSubmit={handleSubmit}>
    </form>
  );
}
به همین جهت، ابتدا کار مدیریت رخ‌داد onSubmit فرم را انجام داده و توسط آن با استفاده از متد preventDefault، از post-back متداول فرم به سمت سرور جلوگیری می‌کنیم. سپس شیء FormData را بر اساس مقادیر حالت متناظر با المان‌های فرم، تشکیل می‌دهیم. کلیدهایی که در اینجا ذکر می‌شوند، نام خواص مدل متناظر سمت سرور را نیز تشکیل خواهند داد.


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

پس از تشکیل شیء FormData در متد مدیریت کننده‌ی handleSubmit، اکنون با استفاده از کتابخانه‌ی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
  // ...

export default function UploadFileSimple() {
  const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket";

  // ...
  const [isUploading, setIsUploading] = useState(false);

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);

    try {
      setIsUploading(true);

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });

      toast.success("Form has been submitted successfully!");

      console.log("uploadResult", data);

      setIsUploading(false);
      setDescription("");
    } catch (error) {
      setIsUploading(false);
      toast.error(error);
    }
  };


  return (
  // ...
  );
}
در اینجا نحوه‌ی ارسال شیء FormData را توسط کتابخانه‌ی axios به سمت سرور مشاهده می‌کنید. با استفاده از متد post آن، به سمت مسیر api/SimpleUpload/SaveTicket که آن‌را در ادامه تکمیل خواهیم کرد، شیء formData متناظر با اطلاعات فرم، به صورت async، ارسال شده‌است. همچنین headers آن نیز به همان «"enctype="multipart/form-data» که پیشتر توضیح داده شد، تنظیم شده‌است.
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده می‌کنید. از آن می‌توان برای فعال و غیرفعال کردن دکمه‌ی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button
   disabled={ isUploading }
   className="btn btn-primary"
   type="submit"
>
  Submit
</button>
به این ترتیب اگر فراخوانی await axios.post هنوز به پایان نرسیده باشد، مقدار isUploading مساوی true بوده و سبب غیرفعال شدن دکمه‌ی submit می‌شود.


اعتبارسنجی سمت کلاینت فایل‌های ارسالی به سمت سرور

در اینجا شاید نیاز باشد نوع و یا اندازه‌ی فایل‌های انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه می‌کنیم:
  const isFileValid = selectedFile => {
    if (!selectedFile) {
      // toast.error("Please select a file.");
      return false;
    }

    const allowedMimeTypes = [
      "image/png",
      "image/jpeg",
      "image/gif",
      "image/svg+xml"
    ];
    if (!allowedMimeTypes.includes(selectedFile.type)) {
      toast.error(`Invalid file type: ${selectedFile.type}`);
      return false;
    }

    const maxFileSize = 1024 * 500;
    const fileSize = selectedFile.size;
    if (fileSize > maxFileSize) {
      toast.error(
        `File size ${(fileSize / 1024).toFixed(
          2
        )} KB must be less than ${maxFileSize / 1024} KB`
      );
      return false;
    }

    return true;
  };
در اینجا ابتدا بررسی می‌شود که آیا فایلی انتخاب شده‌است یا خیر؟ سپس فایل انتخاب شده، باید دارای یکی از MimeTypeهای تعریف شده باشد. همچنین اندازه‌ی آن نیز نباید بیشتر از 500 کیلوبایت باشد. در هر کدام از این موارد، یک خطا توسط react-toastify به کاربر نمایش داده خواهد شد.

اکنون برای استفاده‌ی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کننده‌ی submit اطلاعات:
  const handleSubmit = async event => {
    event.preventDefault();

    if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) {
      return;
    }
در ابتدای متد مدیریت کننده‌ی handleSubmit، متد isFileValid را بر روی دو متغیر حالتی که حاوی اطلاعات فایل‌های انتخابی توسط کاربر هستند، فراخوانی می‌کنیم.

ب) استفاده‌ی از آن جهت غیرفعال کردن دکمه‌ی submit:
<button
            disabled={
              isUploading ||
              !isFileValid(selectedFile1) ||
              !isFileValid(selectedFile2)
            }
            className="btn btn-primary"
            type="submit"
>
   Submit
</button>
می‌توان دقیقا در همان زمانیکه کاربر فایلی را انتخاب می‌کند نیز به انتخاب او واکنش نشان داد. چون مقدار دهی‌های متغیرهای حالت، همواره سبب رندر مجدد فرم می‌شوند و در این حالت مقدار ویژگی disabled نیز محاسبه‌ی مجدد خواهد شد، بنابراین در همان زمانیکه کاربر فایلی را انتخاب می‌کند، متد isFileValid نیز بر روی آن فراخوانی شده و در صورت نیاز، خطایی به او نمایش داده می‌شود.


نمایش درصد پیشرفت آپلود فایل‌ها

کتابخانه‌ی axios، امکان دسترسی به میزان اطلاعات آپلود شده‌ی به سمت سرور را به صورت یک رخ‌داد فراهم کرده‌است که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایل‌ها استفاده می‌کنیم:
      const startTime = Date.now();

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        },
        onUploadProgress: progressEvent => {
          const { loaded, total } = progressEvent;

          const timeElapsed = Date.now() - startTime;
          const uploadSpeed = loaded / (timeElapsed / 1000);

          setUploadProgress({
            queueProgress: Math.round((loaded / total) * 100),
            uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed),
            uploadTimeElapsed: Math.ceil(timeElapsed / 1000),
            uploadSpeed: (uploadSpeed / 1024).toFixed(2)
          });
        }
      });
هر بار که متد رویدادگردان onUploadProgress فراخوانی می‌شود، به همراه اطلاعات شیء progressEvent است که خواص loaded آن به معنای میزان اطلاعات آپلود شده و total هم جمع کل اندازه‌ی اطلاعات در حال ارسال است. بر این اساس و همچنین زمان شروع عملیات، می‌توان اطلاعاتی مانند درصد پیشرفت عملیات، مدت زمان باقیمانده، مدت زمان سپری شده و سرعت آپلود اطلاعات را محاسبه کرد و سپس توسط آن، شیء state ویژه‌ای را به روز رسانی کرد که به صورت زیر تعریف می‌شود:
  const [uploadProgress, setUploadProgress] = useState({
    queueProgress: 0,
    uploadTimeRemaining: 0,
    uploadTimeElapsed: 0,
    uploadSpeed: 0
  });
هر بار به روز رسانی state، سبب رندر مجدد UI می‌شود. به همین جهت متدی را برای رندر جدولی که اطلاعات شیء state فوق را نمایش می‌دهد، به صورت زیر تهیه می‌کنیم:
  const showUploadProgress = () => {
    const {
      queueProgress,
      uploadTimeRemaining,
      uploadTimeElapsed,
      uploadSpeed
    } = uploadProgress;

    if (queueProgress <= 0) {
      return <></>;
    }

    return (
      <table className="table">
        <thead>
          <tr>
            <th width="15%">Event</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <strong>Elapsed time</strong>
            </td>
            <td>{uploadTimeElapsed} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Remaining time</strong>
            </td>
            <td>{uploadTimeRemaining} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Upload speed</strong>
            </td>
            <td>{uploadSpeed} KB/s</td>
          </tr>
          <tr>
            <td>
              <strong>Queue progress</strong>
            </td>
            <td>
              <div
                className="progress-bar progress-bar-info progress-bar-striped"
                role="progressbar"
                aria-valuemin="0"
                aria-valuemax="100"
                aria-valuenow={queueProgress}
                style={{ width: queueProgress + "%" }}
              >
                {queueProgress}%
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    );
  };
و این متد را به این شکل در ذیل المان fieldset فرم، اضافه می‌کنیم تا کار رندر نهایی را انجام دهد:
{showUploadProgress()}
هربار که state به روز می‌شود، مقدار شیء uploadProgress دریافت شده و بر اساس آن، 4 سطر جدول نمایش پیشرفت آپلود، تکمیل می‌شوند.
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است. اگر style آن‌را هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.

یک نکته: اگر می‌خواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگه‌ی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایل‌ها، 100 درصد را مشاهده خواهید کرد.


دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیره‌ی فایل‌های آن‌

بر اساس نحوه‌ی تشکیل FormData سمت کلاینت:
const formData = new FormData();
formData.append("description", description);
formData.append("file1", selectedFile1);
formData.append("file2", selectedFile2);
مدل سمت سرور معادل با آن به صورت زیر خواهد بود:
using Microsoft.AspNetCore.Http;

namespace UploadFilesSample.Models
{
    public class Ticket
    {
        public int Id { set; get; }

        public string Description { set; get; }

        public IFormFile File1 { set; get; }

        public IFormFile File2 { set; get; }
    }
}
که در اینجا هر selectedFile سمت کلاینت، به یک IFormFile سمت سرور نگاشت می‌شود. نام این خواص نیز باید با نام کلیدهای اضافه شده‌ی به دیکشنری FormData، یکی باشند.
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using UploadFilesSample.Models;

namespace UploadFilesSample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SimpleUploadController : Controller
    {
        private readonly IWebHostEnvironment _environment;

        public SimpleUploadController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
        {
            var file1Path = await saveFileAsync(ticket.File1);
            var file2Path = await saveFileAsync(ticket.File2);

            //TODO: save the ticket ... get id

            return Created("", new { id = 1001 });
        }

        private async Task<string> saveFileAsync(IFormFile file)
        {
            const string uploadsFolder = "uploads";
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            //TODO: Do security checks ...!

            if (file == null || file.Length == 0)
            {
                return string.Empty;
            }

            var filePath = Path.Combine(uploadsRootFolder, file.FileName);
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }

            return $"/{uploadsFolder}/{file.Name}";
        }
    }
}
توضیحات تکمیلی:
- تزریق IWebHostEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
ویژگی FromForm نیز مرتبط است به هدر multipart/form-data ارسالی از سمت کلاینت:
      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
React 16x - قسمت 20 - کار با فرم‌ها - بخش 3 - بهبود کیفیت کدهای فرم لاگین
تا اینجا اگر به کدهای کامپوننت فرم لاگینی که ایجاد کردیم دقت کنید، تبدیل شده‌است به محلی برای انباشت حجم قابل توجهی از کد. به این ترتیب اگر قرار باشد فرم‌های جدیدی را تعریف کنیم، نیاز خواهد بود قسمت‌های عمده‌ای از این کدها را در هر جایی تکرار کنیم. بنابراین جهت کاهش مسئولیت‌های آن، نیاز است بازسازی کد (refactoring) قابل ملاحظه‌ای بر روی آن صورت گیرد.


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

قصد داریم قسمت‌هایی از کامپوننت لاگین فعلی را استخراج کرده و آن‌ها را درون یک کامپوننت با قابلیت استفاده‌ی مجدد قرار دهیم:
- خاصیت state: می‌خواهیم تمام فرم‌هایی را که تعریف می‌کنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفاده‌ی مجدد را دارد.
- خاصیت schema: قابلیت استفاده‌ی مجدد را ندارد و مختص فرم لاگین تعریف شده‌است. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفاده‌ی مجدد را دارد. تنها this.state.account آن وابسته‌ی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرم‌ها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعده‌ای را در اینجا تعریف می‌کنیم، بر این مبنا که از این پس، تمام فرم‌های ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم می‌باشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم می‌شود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفاده‌ی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفاده‌ی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر می‌تواند متفاوت باشد.
 - متد handleChange: این متد نیز قابلیت استفاده‌ی مجدد را دارد؛ چون می‌خواهیم در تمام فرم‌ها در حین تایپ اطلاعات، کار اعتبارسنجی ورودی‌ها صورت گیرد. این متد نیز به this.state.account وابسته‌است که قاعده‌ی تعریف خاصیت data در state، می‌تواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابسته‌است به نوع فرمی که مدنظر می‌باشد؛ اما دکمه‌ی submit آن خیر. بجز برچسب دکمه‌ی submit، مابقی قسمت‌های آن مانند کلاس‌های CSS و منطق فعال‌سازی و غیرفعال‌سازی آن، قابلیت استفاده‌ی مجدد را دارند.

بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمت‌های با قابلیت استفاده‌ی مجدد آن، انجام خواهیم داد.


تبدیل قسمت‌های با قابلیت استفاده‌ی مجدد کامپوننت لاگین، به یک کامپوننت عمومی

ابتدا کامپوننت عمومی Form را که قابلیت استفاده‌ی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر می‌دهیم که از آن، بجای کلاس پیش‌فرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شده‌ی در این کامپوننت با قابلیت استفاده‌ی مجدد، در کامپوننت‌های مشتق شده‌ی از آن، در دسترس خواهند بود.

1- در ادامه همانطور که عنوان شد، خاصیت state فرم‌ها باید دارای شیء data و شیء errors باشند تا توسط آن‌ها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
import React, { Component } from "react";

class Form extends Component {
    state = {
        data:{},
        errors:{}
     }
با این تغییر، به فرم login بازگشته و خاصیت account موجود در state آن‌را به data تغییر نام می‌دهیم. برای اینکار بهتر است دکمه‌ی F2 را بر روی نام انتخاب شده‌ی account در VSCode فشار دهید تا تکست باکس تغییر نام آن ظاهر شود. مزیت کار با این ابزار refactoring توکار، اصلاح خودکار تمام ارجاعات به account قبلی، با این نام جدید است. همچنین نام تمام خواصی و متغیرهایی را هم که به account تنظیم کرده بودیم، به data تغییر می‌دهیم تا کار به روز رسانی state بر روی data صورت گیرد و نه account قبلی. در این حالت شاید استفاده از امکانات replace کلی ادیتور، بهتر از استفاده از ویژگی F2 باشد.

2- در ادامه، کاری با خاصیت schema تعریف شده‌ی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آن‌را طور کامل cut کرده و به کامپوننت Form، منتقل می‌کنیم. با این انتقال، چون این متدها از کتابخانه‌ی Joi استفاده می‌کنند، باید import آن‌را نیز به ابتدای ماژول جدید فرم، اضافه کرد:
import Joi from "@hapi/joi";

3- سپس متد رندر کامپوننت Form را کاملا حذف می‌کنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.

4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال داده‌ها به سرور صورت گیرد. به همین جهت آن‌را تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل می‌کنیم.
  doSubmit = () => {
    // call the server
    console.log("Submitted!");
  };

5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل می‌کنیم.

6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام می‌دهیم:
import Form from "./common/form";
// ...

class LoginForm extends Form {

اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آن‌چیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار می‌کند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شده‌است. همچنین اگر دفعه‌ی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخ‌دادی واقع شود.

کدهای کامل کامپوننت فرم را از پیوست انتهای بحث می‌توانید دریافت کنید؛ البته تمام متدهای آن‌را در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.


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

1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمه‌ی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمت‌های آن در تمام فرم‌های دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را می‌توان تبدیل به یک متد کمکی در کلاس Form کرد:
  renderButton(label) {
    return (
      <button disabled={this.validate()} className="btn btn-primary">
        {label}
      </button>
    );
  }
سپس در متد رندر کامپوننت فرم لاگین، تنها کافی است بجای المان button قبلی، از متد فوق استفاده کنیم:
{this.renderButton("Login")}

2- در قسمت‌های قبل، برچسب، فیلدهای ورودی و تگ‌ها و کلاس‌های بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم ساده‌تر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المان‌های Input قرارگرفته‌ی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردان‌های مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق می‌توان به کلاس پایه Form انتقال داد:
  import Input from "./input";
  //...

  renderInput(name, label) {
    const { data, errors } = this.state;
    return (
      <Input
        name={name}
        label={label}
        value={data[name]}
        onChange={this.handleChange}
        error={errors[name]}
      />
    );
همانطور که مشاهده می‌کنید، با استفاده از [] و دسترسی پویای به خواص اشیاء، می‌توان رندر المان Input را تبدیل به متدی با قابلیت نگهداری بهتر کرد و از تکرار ویژگی‌های name ، label ، value ، onChange و error به ازای هر فیلد مورد نیاز، پرهیز کرد. اکنون با این تغییر، متد رندر کامپوننت فرم لاگین به صورت زیر خلاصه می‌شود که بسیار بهتر است از تعریف تعداد قابل ملاحظه‌ای div و کلاس بوت استرپی، تعریف المان‌ها، اتصال تک تک آن‌ها به خواص تعریف شده، اتصال آن‌ها به رویداد گردان‌ها و همچنین به اعتبارسنج‌ها:
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {this.renderInput("username", "Username")}
        {this.renderInput("password", "Password")}
        {this.renderButton("Login")}
      </form>
    );
  }

3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش می‌دهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیش‌فرض پر استفاده، تعریف کرده و به المان Input اعمال می‌کنیم:
  renderInput(name, label, type = "text") {
    const { data, errors } = this.state;
    return (
      <Input
        name={name}
        type={type}
        label={label}
        value={data[name]}
        onChange={this.handleChange}
        error={errors[name]}
      />
    );
  }

سپس این type را در قسمتی که المان مرتبط را رندر می‌کنیم، با password مقدار دهی خواهیم کرد:
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        {this.renderInput("username", "Username")}
        {this.renderInput("password", "Password", "password")}
        {this.renderButton("Login")}
      </form>
    );
}
نیازی به ذکر type، در اولین renderInput ذکر شده، نیست؛ چون مقدار این پارامتر را ازمقدار پیش‌فرض text، دریافت می‌کند.

البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکرده‌ایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال می‌کنیم:
import React from "react";

const Input = ({ name, type, label, value, error, onChange }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        value={value}
        onChange={onChange}
        id={name}
        name={name}
        type={type}
        className="form-control"
      />
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Input;
اکنون اگر تغییرات را ذخیره کرده و به مرورگر مراجعه کنیم، فیلد کلمه‌ی عبور، دیگر حروف وارد شده را نمایش نمی‌دهد و بر اساس نوع استاندارد password، عمل می‌کند.

4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج می‌شود و همچنین هربار باید آن‌را ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت می‌کنند،  هم نام هستند (ویژگی value با مقدار value و ...):
<input
  value={value}
  name={name}
  type={type}
  onChange={onChange}
  id={name}
  className="form-control"
/>
در کامپوننت جاری، منهای پارامترهایی که نام ویژگی‌های تعریف شده، با نام آن پارامترها در تمام قسمت‌های کامپوننت (نه فقط المان input)، یکی نیستند (name، label و error)، مابقی را می‌توان توسط یک «rest operator»، به این متد ارسال کرد:
import React from "react";

const Input = ({ name, label, error, ...rest }) => {
  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input {...rest} name={name} id={name} className="form-control" />
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Input;
بنابراین منهای name، label و error که در قسمت‌های دیگر کامپوننت استفاده می‌شوند، مابقی پارامترهای این کامپوننت تابعی را حذف کرده و با یک rest operator، دریافت می‌کنیم. سپس آن‌ها را به کمک یک spread operator، در المان input، گسترده و درج می‌کنیم. شبیه به اینکار را در قسمت 15 و بخش «ارسال props سفارشی در حین مسیریابی به کامپوننت‌ها» آن انجام داده بودیم. با کمک عملگرهای rest و spread، به سادگی می‌توان هرنوع ویژگی جدیدی را که برای کار با المان input نیاز داریم، به کامپوننت جاری ارسال کرد؛ بدون اینکه نیازی باشد هربار تعریف پارامترهای آن را تغییر دهیم. پارامتر rest تعریف شده، یعنی هر خاصیت دیگری را بجز سه خاصیت name، label و error، به صورت خودکار به این کامپوننت تابعی ارسال کن.
با این تغییر در کامپوننت Input، سایر قسمت‌های برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props می‌شوند (ترتیب ذکر آن‌ها اهمیتی ندارد). مابقی ویژگی‌های تعریف شده‌ی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-20.zip
مطالب
سازگار کردن GridView با افزونه‌های jQuery


افزونه‌ها/پلاگین‌های زیادی جهت کار با table استاندارد HTML وجود دارند و خروجی رندر شده‌ی یک ASP.Net GridView هم در نهایت یک جدول است. فرض کنید قصد داریم افزونه زیر را به GridView استاندارد ASP.Net اعمال کنیم.
jQuery quickSearch plug-in

ظاهرا بدون مشکل خاصی اعمال می‌گردد. برای مثال در هدر صفحه داریم: (شبیه به مثال موجود در سایت اصلی آن، جهت اعمال به GridView1)

<script src="jquery.min.js" type="text/javascript"></script>
<script src="jquery.quicksearch.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
//جستجو در جدول
$('table#<%=GridView1.ClientID %> tbody tr').quicksearch({
position: 'before',
attached: 'span#attachSearch',
labelText: 'جستجو',
isFieldset: true,
loaderText: ' ... ',
fixWidths: true
});
});
</script>

برای اعمال بر:
(در اینجا محل قرارگیری تکست باکس مربوط به جستجو، در span ایی با id مساوی attachSearch تنظیم شده است، می‌توانید از ID خود GridView هم استفاده کنید.)

<span id="attachSearch"></span>
<br />
<asp:GridView ID="GridView1" runat="server"></asp:GridView>

نکته:
1- همانطور که در مقاله مربوط به ClientID ذکر شد، هیچ الزامی ندارد که ID‌ مربوط به GridView‌ شما برای مثال دقیقا همان GridView1 جهت استفاده در سمت کلاینت باشد و بسته به container آن، این نام ترکیبی از ID شیء(های) در بر گیرنده و شیء مورد نظر می‌باشد. به همین جهت از GridView1.ClientID استفاده گردید تا اسکریپت ما با آن مشکلی نداشته باشد.

2- خصوصیات ظاهری افزونه فوق از سلکتور quicksearch فایل css شما دریافت می‌شوند. برای مثال:
.quicksearch
{
width:190px;
}

مشکل!
پس از هر بار جستجو، header مربوط به GridView محو می‌شود، اما نمونه موجود در سایت اصلی افزونه، این مشکل را ندارد. چرا؟!
GridView‌ مربوط به ASP.Net پس از رندر شدن، جدولی است که تگ‌های thead را ندارد. اگر به سورس صفحه سایت افزونه دقت نمائید، هدر جدول با تگ‌های thead محصور شده است اما GridView استاندارد ASP.Net به صورت پیش فرض این‌کار را انجام نمی‌دهد و خروجی آن چیزی شبیه به جدول زیر است: (هدر با th مشخص می‌شود و از تگ thead خبری نیست)

<table >
<tr >
<th scope="col">col1</th>
<th scope="col">col2</th>
<th scope="col">col3</th>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
.
.
.

حداقل دو راه حل برای رفع این مشکل وجود دارد:
الف) راه حل سمت سرور
برای اضافه کردن thead باید کمی کد نویسی کرد. پس از اینکه گرید شما بایند شد، چند سطر زیر را اضافه کنید:

//سازگار با افزونه‌های جی کوئری
if (GridView1.Rows.Count > 0)
{
//This replaces <td> with <th> and adds the scope attribute
GridView1.UseAccessibleHeader = true;

//This will add the <thead> and <tbody> elements
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader;

//This adds the <tfoot> element.
//Remove if you don't have a footer row
//GridView1.FooterRow.TableSection = TableRowSection.TableFooter;
}

ب)راه حل سمت کلاینت
سطر مربوط به جستجو را به صورت زیر هم می‌توان نوشت:

$('table#<%=GridView1.ClientID %> tr:has(td)').quicksearch({

در اینجا به دنبال هرچی tr که td دارد می‌گردیم. به صورت پیش فرض در tr مربوط به هدر گرید، ما th داریم و نه td ، بنابراین مشکل محو شدن هدر به این صورت حل خواهد شد.

نکته:
اگر می‌خواهید که ناحیه مربوط به جستجوی افزونه فوق در پرینت صفحه ظاهر نشود به css صفحه چند سطر زیر را اضافه کنید:

@media print
{
.quicksearch
{
display: none;
}
}


تمرین!
افزونه جی کوئری زیر را به یک ASP.Net GridView اعمال کنید:
table sorter