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> ); };
import 'bootstrap-v4-rtl/dist/css/bootstrap-rtl.css'
<div class="row"> <div className="col-md-3">۱</div> <div className="col-md-6">2</div> <div className="col-md-3">3</div> </div>
- در نمونه بحث جاری کنترل بیشتری بر روی رویدادها خواهید داشت. مثلا قسمت xhr.status == 403 و هدایت کاربر به صفحه لاگین در صورت منقضی شدن اعتبارسنجی آن.
- Ajax.BeginForm برای کار کردن حتما نیاز به submit button داره. در مطلب جاری از یک span هم میتونید استفاده کنید و مشکلی نداره.
- در Ajax.BeginForm آنچنان کنترلی بر روی پردازش نهایی خروجی اکشن متد ندارید. مثلا در اینجا عنوان شد که اگر خروجی JSON بود و اگر دارای فیلد مشخصی با مقدار مشخصی بود نیاز است کار خاصی انجام شود. در حالت jQuery Ajax مستقیم، پردازش JSON سادهتر است.
Controller Factory و Action Invoker وظیفهای مطابق نامشان را عهده دار هستند. اولی برای وهله سازی کنترلرهای مرتبط با درخواست و دومی برای پیدا کردن و تریگر نمودن یک اکشن متد به کار گرفته میشوند. فریم ورک MVC پیاده سازی پیش فرضی را از این دو کامپوننت، به صورت توکار دارد. در طی مقالاتی نحوهی کنترل کردن رفتار پیش فرض این Controller Factory و هم نحوهی جایگزین کرن کامل این کامپوننت را بررسی میکنیم.
ابتدا پروژهی جدیدی را از نوع MVC و با الگوی Empty به نام ControllerExtensibility ایجاد میکنیم. در پوشهی Models یک فایل را به نام Result.cs ساخته و از آن برای معرفی کلاس Result مطابق کدهای ذیل استفاده میکنیم:
namespace ControllerExtensibility.Models { public class Result { public string ControllerName { get; set; } public string ActionName { get; set; } } }
@model ControllerExtensibility.Models.Result @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Result</title> </head> <body> <div>Controller: @Model.ControllerName</div> <div>Action: @Model.ActionName</div> </body> </html>
دو کنترلر را نیز حاوی کدهای زیر ایجاد میکنیم:
کنترلر product
using ControllerExtensibility.Models; using System.Web.Mvc; namespace ControllerExtensibility.Controllers { public class ProductController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Product", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Product", ActionName = "List" }); } } }
کنترلر customer
using System.Web.Mvc; namespace ControllerExtensibility.Controllers { public class CustomerController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Customer", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Customer", ActionName = "List" }); } } }
ایجاد یک Controller Factory سفارشی بهترین راه برای درک نحوهی وهله سازی کنترلرها توسط MVC است. ولی این کار صرفا جنبهی آموزشی داشته و در یک پروژهی واقعی این نوع پیاده سازیها پیشنهاد نمیشود؛ زیرا راههای مفیدتر و سادهتری با پیاده سازی توکار Controller Factory وجود دارند.
Controller Factoryها با پیاده سازی اینترفیس IControllerFactory معرفی میشوند. کدهای این اینترفیس را در ذیل میبینید:
using System.Web.Routing; using System.Web.SessionState; namespace System.Web.Mvc { public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); } }
using System; using System.Web.Mvc; using System.Web.Routing; using System.Web.SessionState; using ControllerExtensibility.Controllers; namespace ControllerExtensibility.Infrastructure { public class CustomControllerFactory : IControllerFactory { public IController CreateController(RequestContext requestContext, string controllerName) { Type targetType = null; switch (controllerName) { case "Product": targetType = typeof (ProductController); break; case "Customer": targetType = typeof (CustomerController); break; default: requestContext.RouteData.Values["controller"] = "Product"; targetType = typeof (ProductController); break; } return targetType == null ? null : (IController) DependencyResolver.Current.GetService(targetType); } public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { return SessionStateBehavior.Default; } public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } } }
نام | نوع | توضیحات |
HttpContext | HttpContextBase | حاوی اطلاعاتی در خصوص درخواست است. |
RouteData | RouteData | حاوی اطلاعاتی در خصوص Rout است که با درخواست رسیده همخوانی دارد. |
یکی از دلایلی که عنوان شد Controller factory سفارشی بدین روش در یک پروژهی عملی به کار گرفته نشود این است که یافتن کلاسهایی از نوع Controller در سراسر برنامه و وهله سازی آنها کار دشواری است. چرا که لازم خواهد بود بتوانید به صورت پویا کنترلر را مکان یابی کرده و بین کلاسهای هم نام در دیگر فضاهای نام تمییز قائل شوید و خطاهای محتمل در حین وهله سازی را کنترل کنید.
در این مثال تنها دو کنترلر داریم و آنها را به صورت مستقیم در Controller Factory وهله سازی میکنیم که در یک پروژهی واقعی مطلوب نیست. ولی آنچه را که این روش آشکارتر میسازد، انعطاف پذیری بالای فریم ورک MVC است که دست ما را برای نفوذ و دخل و تصرف در اعمال و رفتاریهای پیش فرض خود باز گذاشته است و برای مثال در مباحث تزریق وابستگیها و تنظیمات ابتدایی IoC Containers کاربرد دارد.
متد CreateController لازم است وهلهای از کلاسی که اینترفیس IController را پیاده سازی کرده برگرداند؛ در غیر اینصورت کار با خطا متوقف خواهد شد. لذا برای زمانی که درخواست کاربر، هیچ کدام از کنترلرها را مشمول عنایت قرار نمیدهد، باید چارهای اندیشیده شود.
میتوان آن را به کنترلر خاصی که پیغام خطایی را رندر میکند، هدایت کنیم. به عبارت بهتر باید درخواست را به کنترلری که مطمئن هستیم وجود دارد (اصطلاحا کنترلر جانشین) هدایت نماییم. همان طور که در کد فوق در قسمت default میبینید:
default: requestContext.RouteData.Values["controller"] = "Product"; targetType = typeof(ProductController); break;
در MVC انتخاب ویوی مناسب، بر حسب مقدار RouteData.Values صورت میگیرد؛ نه نام کلاس Controller و این سبب خواهد شد فریم ورک، ویوهای مرتبط با کنترلر جانشین شدهی توسط ما را جستجو کند و نه کنترلری که کاربر از طریق URL ورودی آن را درخواست کرده است.
لذا Controller Factory صرفا وظیفه مپ کردن درخواستهای واصله به کنترلرها را ندارد، بلکه توانایی دخل و تصرف در درخواست واصله بر حسب مورد را نیز خواهد داشت.
در نهایت هم نحوهی استفاده از DependencyResolver را برای وهله سازی کلاسهای کنترلر میبینید. متد استاتیک Current یک پیاده سازی از اینترفیس IDependencyResolver را که حاوی متد GetService است، برگشت داده و سپس یک شیء System.Type را به عنوان ورودی گرفته و یک وهلهی ساخته شدهی از آن را به عنوان خروجی برمیگرداند.
متد GetControllerSessionBehavior نیز توسط MVC جهت تعیین اینکه Session data برای کنترلر نیاز است یا خیر به کار گرفته میشود.
متد ReleaseController نیز هر گاه به شیء کنترلر ساخته شده در متد CreateController دیگر نیازی نبود، صدا زده خواهد شد. در کدهای ما ابتدا بررسی میشود آیا اینترفیس IDisposable توسط کلاس، پیاده سازی شده است یا خیر؟ اگر بلی متد Dispose آن جهت آزاد سازی منابعی که میتوانند آزاد شوند، صدا زده میشود.
جهت ثبت Controller Factory ساخته شده در متد Application_Start موجود در فایل global.asax.cs بوسیله کلاس ControllerBuilder و مطابق کدهای ذیل عمل مینماییم:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; using ControllerExtensibility.Infrastructure; namespace ControllerExtensibility { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory()); } } }
HttpContext.Request.IsAjaxRequest();
و ویژگیهای درخواست توسط سرویس http - AngularJs$ :
همینطور که میبینید، در هدر درخواست http$ یک مورد مفقود الاثر شده به نام X-Requested-With داریم و همین مقدار است که مشخص میکند این یک درخواست ایجکسی است یا خیر و اکستنشن ()IsAjaxRequest نیر با همین مقدار عمل تشخیص را انجام میدهد. و به همین خاطر بود که این متد مقدار False را برمیگرداند.
بعد از کمی جستجو در این مورد ، به مخزن git انگیولار رسیدم و به صراحت به این موضوع اشاره شده بود که این هدر به صورت پیشفرض از درخواستهای http$ برداشته شده است.
بنابراین تنها راه حل این بود که خودمان به صورت دستی این هدر خاص رو به ماژول برنامه اضافه کنیم. به صورت زیر :
myAppModule.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest'; }]);
با اضافه کردن این هدر به درخواستهای http$ ، اکستنشن ()IsAjaxRequest مقدار درست را برمیگرداند.
- تنظیمات پیش فرض باید تغییر کنند تا کلمات عبور حداقل 10 کاراکتر باشند
- کلمه عبور حداقل یک عدد و یک کاراکتر ویژه باید داشته باشد
- امکان استفاده از 5 کلمه عبور اخیری که ثبت شده وجود ندارد
ایجاد اپلیکیشن جدید
در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.
بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک میکند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.
- کنترلر Account از کلاس UserManager استفاده میکند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده میکند که برای دسترسی و مدیریت دادههای کاربران استفاده میشود. در مثال ما این کلاس از Entity Framework استفاده میکند که پیاده سازی پیش فرض است.
- متد Register POST یک کاربر جدید میسازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی میکند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); }
قانون 1: کلمههای عبور باید حداقل 10 کاراکتر باشند
- مقدار حداقل کاراکترهای کلمه عبور به دو شکل میتواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی میشود، همینجا میتوانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری میکند. سپس میتوان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext())) { PasswordValidator = new MinimumLengthValidator (10); } }
- حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
public AccountController() : this(new ApplicationUserManager()) { } public AccountController(ApplicationUserManager userManager) { UserManager = userManager; } public ApplicationUserManager UserManager { get; private set; }
- اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت میکنید.
قانون 2: کلمههای عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند
- در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده میکنیم.
public class CustomPasswordValidator : IIdentityValidator<string> { public int RequiredLength { get; set; } public CustomPasswordValidator(int length) { RequiredLength = length; } public Task<IdentityResult> ValidateAsync(string item) { if (String.IsNullOrEmpty(item) || item.Length < RequiredLength) { return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength))); } string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$"; if (!Regex.IsMatch(item, pattern)) { return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character")); } return Task.FromResult(IdentityResult.Success); }
- قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } }
قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد
public class PreviousPassword { public PreviousPassword() { CreateDate = DateTimeOffset.Now; } [Key, Column(Order = 0)] public string PasswordHash { get; set; } public DateTimeOffset CreateDate { get; set; } [Key, Column(Order = 1)] public string UserId { get; set; } public virtual ApplicationUser User { get; set; } }
- خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser { public ApplicationUser() : base() { PreviousUserPasswords = new List<PreviousPassword>(); } public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; } }
public class ApplicationUserStore : UserStore<ApplicationUser> { public ApplicationUserStore(DbContext context) : base(context) { } public override async Task CreateAsync(ApplicationUser user) { await base.CreateAsync(user); await AddToPreviousPasswordsAsync(user, user.PasswordHash); } public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password) { user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password }); return UpdateAsync(user); } }
public class ApplicationUserManager : UserManager<ApplicationUser> { private const int PASSWORD_HISTORY_LIMIT = 5; public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ResetPasswordAsync(userId, token, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } private async Task<bool> IsPreviousPassword(string userId, string newPassword) { var user = await FindByIdAsync(userId); if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate). Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT) .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any()) { return true; } return false; } }
سورس کد این مثال را میتوانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.
بررسی سیستم جدید گرید بوت استرپ 3
<div class="container"> <div class="alert alert-info"> <h4> ایجاد فاصله بین ستونها </h4> </div> <div class="row"> <div class="col-lg-3 col-sm-4"> <div class="alert alert-danger" role="alert"> ستون اول </div> </div> <div class="col-lg-8 col-lg-offset-1 col-sm-7 col-sm-offset-1"> <div class="alert alert-success" role="alert"> ستون دوم </div> </div> </div> <!-- end row --> </div>
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Threading; class Program { static int a = 0; static int b = 0; static Random random = new Random(); static void Main(string[] args) { Thread obj = new Thread(Division); obj.Start(); Division(); } static void Division() { for (int i = 0; i <= 500; i++) { try { //Choosing random numbers between 1 to 5 a = random.Next(1, 10); b = random.Next(1, 10); //Dividing double ans = a / b; //Reset Variables a = 0; b = 0; Console.WriteLine("Answer : {0} --> {1}", i, ans); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } } }
همانطور که در کد بالا ملاحظه میکنید، متد Division به صورت Thread Safe پیاده سازی نشدهاست! اما مشکل کجاست!؟
class Program { static int a = 0; static int b = 0; static Random random = new Random(); static readonly object _object = new object(); static void Main(string[] args) { Thread obj = new Thread(Division); obj.Start(); Division(); } static void Division() { for (int i = 0; i <= 500; i++) { try { Monitor.Enter(_object); //Choosing random numbers between 1 to 5 a = random.Next(1, 10); b = random.Next(1, 10); //Dividing double ans = a / b; //Reset Variables a = 0; b = 0; Console.WriteLine("Answer : {0} --> {1}", i, ans); Monitor.Exit(_object); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } } }
مادامی که یک Thread در حالت انتخاب اعداد تصادفی تا تقسیم و اعلام نتیجه میباشد، به Threadهای داخلی دیگر، اجازهی ورود به این بخش که تحت کنترل Monitor میباشد داده نخواهد شد. همانطور که گفته شده، بازهی تحت کنترل مانیتور میتواند با بلاک Lock(object) جایگزین شود. شیء object یک شیء مشترک (static) میان تمام اشیاء است برای کنترل ورود Threadها و قفل گزاری مشترک بین این اشیاء.
static void Main() { using (var mutex = new Mutex (false, "dotnettips.info Demo")) { if (!mutex.WaitOne (TimeSpan.FromSeconds (3), false)) { Console.WriteLine ("Another app instance is running. Bye!"); return; } RunProgram(); } } static void RunProgram() { Console.WriteLine ("Running. Press Enter to exit"); Console.ReadLine(); }