A view requests from the model the information that it needs to generate an output representation.
public interface IShoppingCartService { ShoppingCart GetContents(); ShoppingCart AddItemToCart(int itemId, int quantity); } public class ShoppingCartService : IShoppingCartService { public ShoppingCart GetContents() { throw new NotImplementedException("Get cart from Persistence Layer"); } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException("Add Item to cart then return updated cart"); } } public class ShoppingCart { public List<product> Items { get; set; } } public class Product { public int ItemId { get; set; } public string ItemName { get; set; } } public class ShoppingCartController : Controller { //Concrete object below points to actual service //private ShoppingCartService _shoppingCartService; //loosely coupled code below uses the interface rather than the //concrete object private IShoppingCartService _shoppingCartService; public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } public ActionResult GetCart() { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.GetContents(); return View("Cart", cart); } public ActionResult AddItemToCart(int itemId, int quantity) { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity); return View("Cart", cart); } }
//loosely coupled code below uses the interface rather //than the concrete object private IShoppingCartService _shoppingCartService; //MVC uses this constructor public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } //You can use this constructor when testing to inject the //ShoppingCartService dependency public ShoppingCartController(IShoppingCartService shoppingCartService) { _shoppingCartService = shoppingCartService; }
[TestClass] public class ShoppingCartControllerTests { [TestMethod] public void GetCartSmokeTest() { //arrange ShoppingCartController controller = new ShoppingCartController(new ShoppingCartServiceStub()); // Act ActionResult result = controller.GetCart(); // Assert Assert.IsInstanceOfType(result, typeof(ViewResult)); } } /// <summary> /// This is is a stub of the ShoppingCartService /// </summary> public class ShoppingCartServiceStub : IShoppingCartService { public ShoppingCart GetContents() { return new ShoppingCart { Items = new List<product> { new Product {ItemId = 1, ItemName = "Widget"} } }; } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException(); } }
پیشنیازهای اجرای پروژهی DNT Identity
- ابتدا نیاز است حداقل ASP.NET Core Identity 1.1 را نصب کرده باشید.
- همچنین بانک اطلاعاتی پایهی آن که به صورت خودکار در اولین بار اجرای برنامه تشکیل میشود، مبتنی بر LocalDB است. بنابراین اگر قصد تغییری را در تنظیمات Context آن ندارید، بهتر است LocalDB را نیز بر روی سیستم نصب کنید. هرچند با تغییر تنظیم ActiveDatabase به SqlServer در فایل appsettings.json، برنامه به صورت خودکار از نگارش کامل SqlServer استفاده خواهد کرد. رشتهی اتصالی آن نیز در مدخل ConnectionStrings فایل appsettings.json ذکر شدهاست و قابل تغییر است. برای شروع به کار، نیازی به اجرای مراحل Migrations را نیز ندارید و همینقدر که برنامه را اجرا کنید، بانک اطلاعاتی آن نیز تشکیل خواهد شد.
- کاربر پیش فرض Admin سیستم و کلمهی عبور آن از مدخل AdminUserSeed فایل appsettings.json خوانده میشوند.
- تنظیمات ایمیل پیش فرض برنامه به استفادهی از PickupFolder در مدخل Smtp فایل appsettings.json تنظیم شدهاست. بنابراین تمام ایمیلهای برنامه را جهت آزمایش محلی میتوانید در مسیر PickupFolder آن یا همان c:\smtppickup مشاهده کنید. محتوای این ایمیلها را نیز توسط مرورگر (drag&drop بر روی یک tab جدید) و یا برنامهی Outlook میتوان مشاهده کرد.
سفارشی سازی کلید اصلی موجودیتهای ASP.NET Core Identity
ASP.NET Core Identity به همراه دو سری موجودیت است. یک سری سادهی آن که از یک string به عنوان نوع کلید اصلی استفاده میکنند و سری دوم، حالت جنریک که در آن میتوان نوع کلید اصلی را به صورت صریحی قید کرد و تغییر داد. در اینجا نیز قصد داریم از حالت جنریک استفاده کرده و نوع کلید اصلی جداول را تغییر دهیم. تمام این موجودیتهای تغییر یافته را در پوشهی src\ASPNETCoreIdentitySample.Entities\Identity نیز میتوانید مشاهده کنید و شامل موارد ذیل هستند:
جدول نقشهای سیستم
public class Role : IdentityRole<int, UserRole, RoleClaim>, IAuditableEntity { public Role() { } public Role(string name) : this() { Name = name; } public Role(string name, string description) : this(name) { Description = description; } public string Description { get; set; } }
در اولین بار اجرای برنامه، نقش Admin در این جدول ثبت خواهد شد.
جدول کاربران منتسب به نقشها
public class UserRole : IdentityUserRole<int>, IAuditableEntity { public virtual User User { get; set; } public virtual Role Role { get; set; } }
در اولین بار اجرای برنامه، کاربر شماره 1 یا همان Admin به نقش شماره 1 یا همان Admin، انتساب داده میشود.
جدول جدید IdentityRoleClaim سیستم
public class RoleClaim : IdentityRoleClaim<int>, IAuditableEntity { public virtual Role Role { get; set; } }
جدول UserClaim سیستم
public class UserClaim : IdentityUserClaim<int>, IAuditableEntity { public virtual User User { get; set; } }
جداول توکن و لاگینهای کاربران
public class UserToken : IdentityUserToken<int>, IAuditableEntity { public virtual User User { get; set; } } public class UserLogin : IdentityUserLogin<int>, IAuditableEntity { public virtual User User { get; set; } }
جدول کاربران سیستم
public class User : IdentityUser<int, UserClaim, UserRole, UserLogin>, IAuditableEntity { public User() { UserUsedPasswords = new HashSet<UserUsedPassword>(); UserTokens = new HashSet<UserToken>(); } [StringLength(450)] public string FirstName { get; set; } [StringLength(450)] public string LastName { get; set; } [NotMapped] public string DisplayName { get { var displayName = $"{FirstName} {LastName}"; return string.IsNullOrWhiteSpace(displayName) ? UserName : displayName; } } [StringLength(450)] public string PhotoFileName { get; set; } public DateTimeOffset? BirthDate { get; set; } public DateTimeOffset? CreatedDateTime { get; set; } public DateTimeOffset? LastVisitDateTime { get; set; } public bool IsEmailPublic { get; set; } public string Location { set; get; } public bool IsActive { get; set; } = true; public virtual ICollection<UserUsedPassword> UserUsedPasswords { get; set; } public virtual ICollection<UserToken> UserTokens { get; set; } } public class UserUsedPassword : IAuditableEntity { public int Id { get; set; } public string HashedPassword { get; set; } public virtual User User { get; set; } public int UserId { get; set; } }
فیلد IsActive نیز از این جهت اضافه شدهاست تا بجای حذف فیزیکی یک کاربر، بتوان اکانت او را غیرفعال کرد.
تعریف Shadow properties ثبت تغییرات رکوردها
در #C ارثبری چندگانهی کلاسها ممنوع است؛ مگر اینکه از اینترفیسها استفاده شود. برای مثال IdentityUser یک کلاس است و در اینجا دیگر نمیتوان کلاس دومی را به نام BaseEntity جهت اعمال خواص اضافهتری اعمال کرد. به همین جهت است که اعمال اینترفیس خالی IAuditableEntity را در اینجا مشاهده میکنید. این اینترفیس کار علامتگذاری کلاسهایی را انجام میدهد که قصد داریم به آنها به صورت خودکار، خواصی مانند تاریخ ثبت رکورد، تاریخ ویرایش آن و غیره را اعمال کنیم.
در Context برنامه، به اطلاعات src\ASPNETCoreIdentitySample.Entities\AuditableEntity مراجعه شده و متد AddAuditableShadowProperties بر روی تمام کلاسهایی از نوع IAuditableEntity اعمال میشود. این متد خواص مدنظر ما را مانند ModifiedDateTime به صورت Shadow properties به موجودیتهای علامتگذاری شده اضافه میکند.
همچنین متد SetAuditableEntityPropertyValues، کار مقدار دهی خودکار این خواص را انجام خواهد داد. بنابراین دیگر نیازی نیست در برنامه برای مثال IP شخص ثبت کننده یا ویرایش کننده را به صورت دستی مقدار دهی کرد. هم تعریف و هم مقدار دهی آن توسط Change tracker سیستم به صورت خودکار انجام خواهند شد.
تاثیر افزودن Shadow properties را بر روی کلاس نقشهای سیستم، در تصویر فوق ملاحظه میکنید. خواصی که به صورت معمول در کلاسهای برنامه ظاهر نمیشوند و صرفا هدف بازبینی سیستم را برآورده میکنند و مدیریت آنها نیز در اینجا کاملا خودکار است.
سفارشی سازی DbContext برنامه
نحوهی سفارشی سازی DbContext برنامه را در پوشهی src\ASPNETCoreIdentitySample.DataLayer\Context و src\ASPNETCoreIdentitySample.DataLayer\Mappings ملاحظه میکنید. پوشهی Context حاوی کلاس ApplicationDbContextBase است که تمام سفارشی سازیهای لازم بر روی آن انجام شدهاست؛ شامل:
- تغییر نوع کلید اصلی موجودیتها به همراه معرفی موجودیتهای تغییر یافته:
public abstract class ApplicationDbContextBase : IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IUnitOfWork
- اعمال متد BeforeSaveTriggers به تمام نگارشهای مختلف SaveChanges
protected void BeforeSaveTriggers() { ValidateEntities(); SetShadowProperties(); this.ApplyCorrectYeKe(); }
- انتخاب نوع بانک اطلاعاتی مورد استفاده در متد OnConfiguring
در اینجا است که خاصیت ActiveDatabase تنظیم شدهی در فایل appsettings.json خوانده شده و اعمال میشوند. تعریف متد GetDbConnectionString را در کلاس SiteSettingsExtesnsions مشاهده میکنید. کار آن استفادهی از بانک اطلاعاتی درون حافظهای، جهت انجام آزمونهای واحد و یا استفادهی از LocalDb و یا نگارش کامل SQL Server میباشد. اگر علاقمند بودید تا بانک اطلاعاتی دیگری (مثلا SQLite) را نیز اضافه کنید، ابتدا enum ایی به نام ActiveDatabase را تغییر داده و سپس متد GetDbConnectionString و متد OnConfiguring را جهت درج اطلاعات این بانک اطلاعاتی جدید، اصلاح کنید.
پس از تعریف این DbContext پایهی سفارشی سازی شده، کلاس جدید ApplicationDbContext را مشاهده میکنید. این کلاس Context ایی است که در برنامه از آن استفاده میشود و از کلاس پایه ApplicationDbContextBase مشتق شدهاست:
public class ApplicationDbContext : ApplicationDbContextBase
تنظیمات mapping آنها نیز به متد OnModelCreating این کلاس اضافه خواهند شد. فقط نحوهی استفادهی از آن را بهخاطر داشته باشید:
protected override void OnModelCreating(ModelBuilder builder) { // it should be placed here, otherwise it will rewrite the following settings! base.OnModelCreating(builder); // Adds all of the ASP.NET Core Identity related mappings at once. builder.AddCustomIdentityMappings(SiteSettings.Value); // Custom application mappings // This should be placed here, at the end. builder.AddAuditableShadowProperties(); }
سپس متد AddCustomIdentityMappings ذکر شدهاست. این متد اطلاعات src\ASPNETCoreIdentitySample.DataLayer\Mappings را به صورت خودکار و یکجا اضافه میکند که در آن برای مثال نام جداول پیش فرض Identity سفارشی سازی شدهاند.
در آخر باید AddAuditableShadowProperties فراخوانی شود تا خواص سایهای که پیشتر در مورد آنها بحث شد، به سیستم به صورت خودکار اضافه شوند.
تمام نگاشتهای سفارشی شما باید در این میان و در قسمت «Custom application mappings» درج شوند.
در قسمت بعدی، نحوهی سفارشی سازی سرویسهای پایهی Identity را بررسی خواهیم کرد. بدون این سفارشی سازی و اطلاعات رسانی به سرویسهای پایه که از چه موجودیتهای جدید سفارشی سازی شدهایی در حال استفاده هستیم، کار Migrations انجام نخواهد شد.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
مدتی قبل مطلبی تحت عنوان "What’s coming in the next version of ASP.NET Webforms" منتشر شد (که نویسنده آن دقیقا مشخص نیست این اطلاعات را از کجا آورده و همچنین تکذیبیهای هم جایی در مورد آن صادر نشد ...)؛ بنابراین خلاصهای از آنرا با هم مرور خواهیم کرد:
اخیرا تمام توجه تیم ASP.NET معطوف نسخهی MVC آن شده است؛ هر چند هنوز تعداد قابل توجهی از پروژههای ASP.NET بر اساس Webforms تهیه شدهاند یا میشوند. همچنین برخلاف مطالب منتشره در انجمنها یا بلاگهای مرتبط، تیم ASP.NET ، نگارش Webforms را فراموش نکرده و حتی نگارش 4 آن نیز تعدادی از قابلیتهای MVC مانند URL Routing، حجم کمتر ViewState و کنترل بیشتر بر روی HTML نهایی را به همراه داشته است.
به روز رسانیهای متوالی MVC (که اکنون به نگارش 3 رسیده است)، شاید این تصور را پیش آورده باشد که دیگر Webforms مرده است! اما مهترین دلیل به روز رسانیهای دیر هنگام نسخهی Webforms ، یکی بودن اسمبلیهای آن با مجموعهی اصلی دات نت فریم ورک است (برخلاف نسخهی MVC که به صورت افزونهای برای این مجموعه ارائه شده است).
نسخهی بعدی Webforms (حداقل) شامل تازهها و پیشرفتهای زیر خواهد بود:
MVC ModelBinders
در نسخهی MVC مفهومی به نام Model binders وجود دارد. کار آن مقدار دهی مدل برنامه به صورت خودکار بر اساس اطلاعات وارد شده توسط کاربر در رابط کاربری برنامه است. برای مثال در Webforms داریم employee.Name = txtName.Text . به این معنا که مقدر Text یک جعبهی متنی به نام txtName را به خاصیت Name شیء employee نسبت بده. اینکار (انقیاد اطلاعات رابط کاربر به مدل برنامه) با وجود Model binders در نسخهی MVC به صورت خودکار انجام میشود. این مورد دو مزیت عمده را به همراه خواهد داشت: الف) سادگی و حجم کمتر کد ب) امکان تهیه سادهتر unit test جهت قسمتهای مختلف برنامه (چون دیگر به txtName گره نخواهد خورد).
امکانات Model binders ، گفته شده (مطابق مرجع فوق!) که قرار است جزئی از نگارش بعدی Webforms باشد ... (امیدوارم!)
بهبودهای حاصل شده در اعتبار سنجی
نسخهی بعدی Webforms شامل پیشرفتهای اعتبارسنجی نسخهی MVC نیز خواهد بود. به این معنا که امکان کنارگذاشتن کنترلهای اعتبار سنجی Webforms و استفاده یکپارچه از امکانات jQuery فراهم خواهد شد (به این صورت دیگر شما محدود به یک سری کنترل از پیش تعیین شده نخواهید بود و امکان دسترسی به کوهی از افزونههای اعتبار سنجی jQuery را خواهید داشت).
CSS Sprites
CSS Sprites که در نگارش بعدی Webforms پشتیبانی خواهد شد (+)، تکنیکی است جهت کاهش تعداد رفت و برگشتهای به سرور با ارائهی یک فایل حاوی تمام تصاویر قرار گرفته شده در یک شبکه یا گرید. به این صورت بجای دها یا صدها رفت و برگشت به سرور جهت دریافت تصاویر یک صفحه، تنها یک رفت و برگشت انجام خواهد شد.
- Get/GetAsync(with data retriever)
- Get/GetAsync(without data retriever)
- Set/SetAsync
- Remove/RemoveAsync
- ~~Refresh/RefreshAsync (was removed)~~
- RemoveByPrefix/RemoveByPrefixAsync
- SetAll/SetAllAsync
- GetAll/GetAllAsync
- GetByPrefix/GetByPrefixAsync
- RemoveAll/RemoveAllAsync
- GetCount
- Flush/FlushAsync
- TrySet/TrySetAsync
- GetExpiration/GetExpirationAsync
- In-Memory
- Memcached
- Redis(Based on StackExchange.Redis)
- Redis(Based on csredis)
- SQLite
- Hybrid
- Disk
- LiteDb
- BinaryFormatter
- MessagePack
- Newtonsoft.Json
- Protobuf
- System.Text.Json
Install-Package EasyCaching.InMemory
services.AddEasyCaching(options => { // use memory cache with a simple way options.UseInMemory(); }
// تنظیم یک کش با کلید - مقدار - زمان انقضا void Set<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task SetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // تنظیم یک کش با مقدار و زمان انقضا که تایپ مقدار از نوع دیکشنری هست و کلید دیکشنری بعنوان کلید کش قرار میگیرد void SetAll<T>(IDictionary<string, T> value, TimeSpan expiration); Task SetAllAsync<T>(IDictionary<string, T> value, TimeSpan expiration); // تنظیم یک کش با کلید - مقدار - زمان انقضا // اگر کلیدی همنام وجود داشته باشد مقدار نادرست و در غیر اینصورت مقدار نادرست را برمیگرداند bool TrySet<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task<bool> TrySetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // گرفتن یک کش با کلید CacheValue<T> Get<T>(string cacheKey); Task<CacheValue<T>> GetAsync<T>(string cacheKey); // CacheValue<T> Get<T>(string cacheKey, Func<T> dataRetriever, TimeSpan expiration); Task<CacheValue<T>> GetAsync<T>(string cacheKey, Func<Task<T>> dataRetriever, TimeSpan expiration); // گرفتن یک کش با چند کاراکتر پیشین کلید آن // برای مثال یک کلید با نام // MyKey // تنها با داشتن چند حرف اول // MyK // میتوانیم این کش را دریافت کنیم IDictionary<string, CacheValue<T>> GetByPrefix<T>(string prefix); Task<IDictionary<string, CacheValue<T>>> GetByPrefixAsync<T>(string prefix); // IDictionary<string, CacheValue<T>> GetAll<T>(IEnumerable<string> cacheKeys); Task<IDictionary<string, CacheValue<T>>> GetAllAsync<T>(IEnumerable<string> cacheKeys); // گرفتن تعداد کشهای با کاراکترهای پیشین کلید که میان چند کلید یکسان است int GetCount(string prefix = ""); Task<int> GetCountAsync(string prefix = ""); // گرفتن زمان انقضا باقیمانده از یک کش با کلید آن TimeSpan GetExpiration(string cacheKey); Task<TimeSpan> GetExpirationAsync(string cacheKey); // حذف کردن یک کش با کلید void Remove(string cacheKey); Task RemoveAsync(string cacheKey); // حذف کردن یک کش با چند کاراکتر پیشین کلید void RemoveByPrefix(string prefix); Task RemoveByPrefixAsync(string prefix); // حذف کردن چند کش با لیستی از کلیدها void RemoveAll(IEnumerable<string> cacheKeys); Task RemoveAllAsync(IEnumerable<string> cacheKeys); // بررسی وجود یا عدم وجود یک کش با کلید bool Exists(string cacheKey); Task<bool> ExistsAsync(string cacheKey); // حذف کردن همه کشها void Flush(); Task FlushAsync();
Install-Package EasyCaching.Redis
services.AddEasyCaching(option => { option.UseRedis(config => { config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
- متدهای Keys
// حذف کردن یک کلید در صورت وجود bool KeyDel(string cacheKey); Task<bool> KeyDelAsync(string cacheKey); // تنظیم تاریخ انتضا به یک کلید موجود بر حسب ثانیه bool KeyExpire(string cacheKey, int second); Task<bool> KeyExpireAsync(string cacheKey, int second); // بررسی وجود یا عدم وجود یک کلید bool KeyExists(string cacheKey); Task<bool> KeyExistsAsync(string cacheKey); // گرفتن زمان انتقضا باقیمانده یک کلید long TTL(string cacheKey); Task<long> TTLAsync(string cacheKey); // جستجو بین همه کلیدها براساس فیلتر شامل بودن نام کلید از مقدار ورودی List<string> SearchKeys(string cacheKey, int? count = null);
- متدهای String
// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید long IncrBy(string cacheKey, long value = 1); Task<long> IncrByAsync(string cacheKey, long value = 1); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید double IncrByFloat(string cacheKey, double value = 1); Task<double> IncrByFloatAsync(string cacheKey, double value = 1); // تنظیم یک کلید و مقدار وقتی مقدار از نوع رشته باشد bool StringSet(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); Task<bool> StringSetAsync(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); // گرفتن کلید و مقدار آن وقتی مقدار از نوع رشته باشد string StringGet(string cacheKey); Task<string> StringGetAsync(string cacheKey); // گرفتن تعداد کاراکترهای مقدار یک کلید وقتی مقدار از نوع رشته باشد long StringLen(string cacheKey); Task<long> StringLenAsync(string cacheKey); // جایگزاری یک رشته درون رشته مقدار یک کلید بعد از شماره کاراکتر مشخص شده در ورودی برای مثال // "Hello World" // 6 , jack // "Hello jack" long StringSetRange(string cacheKey, long offest, string value); Task<long> StringSetRangeAsync(string cacheKey, long offest, string value); // گرفتن یک بازه از رشته مقدار یک کلید با شماره کاراکتر شروع و پایان string StringGetRange(string cacheKey, long start, long end); Task<string> StringGetRangeAsync(string cacheKey, long start, long end);
- متدهای Hashes
// شما میتوانید دو کلید با نامهای یکسان داشته باشید که در کلید تایپ دیکشنری مقدار خود باهم متفاوت هستند bool HMSet(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); Task<bool> HMSetAsync(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); // شما میتوانید دو کلید با نامهای یکسان داشته باشید که در ورودی فیلد باهم متفاوت هستند bool HSet(string cacheKey, string field, string cacheValue); Task<bool> HSetAsync(string cacheKey, string field, string cacheValue); // بررسی وجود یا عدم وجود یک کلید و فیلد bool HExists(string cacheKey, string field); Task<bool> HExistsAsync(string cacheKey, string field); // حذف کردن کلیدهای همنام موجود با همه فیلدهای متفاوت در حالت پیشفرض مگر اینکه کلید و نام فیلد را بهمراه آن مشخص کنید long HDel(string cacheKey, IList<string> fields = null); Task<long> HDelAsync(string cacheKey, IList<string> fields = null); // گرفتن مقدار با نام کلید و نام فیلد string HGet(string cacheKey, string field); Task<string> HGetAsync(string cacheKey, string field); // گرفتن فیلد و مقدار با کلید Dictionary<string, string> HGetAll(string cacheKey); Task<Dictionary<string, string>> HGetAllAsync(string cacheKey); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید و فیلد long HIncrBy(string cacheKey, string field, long val = 1); Task<long> HIncrByAsync(string cacheKey, string field, long val = 1); // گرفتن فیلدهای متفاوت یک کلید List<string> HKeys(string cacheKey); Task<List<string>> HKeysAsync(string cacheKey); // گرفتن تعداد فیلدهای متفاوت یک کلید long HLen(string cacheKey); Task<long> HLenAsync(string cacheKey); // گرفتن مقادیر یک کلید بدون در نظر گرفتن فیلدهای متفاوت List<string> HVals(string cacheKey); Task<List<string>> HValsAsync(string cacheKey); // گرفتن مقدار دیکشنری با کلید و نام فیلدها Dictionary<string, string> HMGet(string cacheKey, IList<string> fields); Task<Dictionary<string, string>> HMGetAsync(string cacheKey, IList<string> fields);
- متدهای List
// گرفتن یک مقدار از لیست مقادیر با شماره ایندکس آن T LIndex<T>(string cacheKey, long index); Task<T> LIndexAsync<T>(string cacheKey, long index); // گرفتن تعداد مقادیر در لیست یک کلید long LLen(string cacheKey); Task<long> LLenAsync(string cacheKey); // گرفتن اولین مقدار از مقادیر یک لیست در یک کلید T LPop<T>(string cacheKey); Task<T> LPopAsync<T>(string cacheKey); // ایجاد یک کلید که لیستی از مقادیر را پشتیبانی میکند و میتوانید هر بار مقدار جدید به لیست آن اضافه کنید long LPush<T>(string cacheKey, IList<T> cacheValues); Task<long> LPushAsync<T>(string cacheKey, IList<T> cacheValues); // گرفتن مقادیر یک لیست از داده بر اساس شماره ایندکس شروع و پایان برای مثال مقادیر ۳ تا ۷ از ۱۰ مقدار List<T> LRange<T>(string cacheKey, long start, long stop); Task<List<T>> LRangeAsync<T>(string cacheKey, long start, long stop); // حذف کردن مقادیر یک لیست بر اساس تعداد وارد شده که بعد از مقدار وارد شده شروع به شمارش میشود long LRem<T>(string cacheKey, long count, T cacheValue); Task<long> LRemAsync<T>(string cacheKey, long count, T cacheValue); // افزودن یک مقدار به لیستی از مقادیر یک کلید با گرفتن شماره ایندکس bool LSet<T>(string cacheKey, long index, T cacheValue); Task<bool> LSetAsync<T>(string cacheKey, long index, T cacheValue); // بررسی میکند که لیست مقداری برای شماره ایندکس شروع و پایان درون خودش دارد یا خیر bool LTrim(string cacheKey, long start, long stop); Task<bool> LTrimAsync(string cacheKey, long start, long stop); // https://redis.io/commands/lpushx long LPushX<T>(string cacheKey, T cacheValue); Task<long> LPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/linsert long LInsertBefore<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertBeforeAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/linsert long LInsertAfter<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertAfterAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/rpushx long RPushX<T>(string cacheKey, T cacheValue); Task<long> RPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/rpush long RPush<T>(string cacheKey, IList<T> cacheValues); Task<long> RPushAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/rpop T RPop<T>(string cacheKey); Task<T> RPopAsync<T>(string cacheKey);
- متدهای Set
// https://redis.io/commands/SAdd long SAdd<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); Task<long> SAddAsync<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); // https://redis.io/commands/SCard long SCard(string cacheKey); Task<long> SCardAsync(string cacheKey); // https://redis.io/commands/SIsMember bool SIsMember<T>(string cacheKey, T cacheValue); Task<bool> SIsMemberAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/SMembers List<T> SMembers<T>(string cacheKey); Task<List<T>> SMembersAsync<T>(string cacheKey); // https://redis.io/commands/SPop T SPop<T>(string cacheKey); Task<T> SPopAsync<T>(string cacheKey); // https://redis.io/commands/SRandMember List<T> SRandMember<T>(string cacheKey, int count = 1); Task<List<T>> SRandMemberAsync<T>(string cacheKey, int count = 1); // https://redis.io/commands/SRem long SRem<T>(string cacheKey, IList<T> cacheValues = null); Task<long> SRemAsync<T>(string cacheKey, IList<T> cacheValues = null);
- متدهای Stored Set
// https://redis.io/commands/ZAdd long ZAdd<T>(string cacheKey, Dictionary<T, double> cacheValues); Task<long> ZAddAsync<T>(string cacheKey, Dictionary<T, double> cacheValues); // https://redis.io/commands/ZCard long ZCard(string cacheKey); Task<long> ZCardAsync(string cacheKey); // https://redis.io/commands/ZCount long ZCount(string cacheKey, double min, double max); Task<long> ZCountAsync(string cacheKey, double min, double max); // https://redis.io/commands/ZIncrBy double ZIncrBy(string cacheKey, string field, double val = 1); Task<double> ZIncrByAsync(string cacheKey, string field, double val = 1); // https://redis.io/commands/ZLexCount long ZLexCount(string cacheKey, string min, string max); Task<long> ZLexCountAsync(string cacheKey, string min, string max); // https://redis.io/commands/ZRange List<T> ZRange<T>(string cacheKey, long start, long stop); Task<List<T>> ZRangeAsync<T>(string cacheKey, long start, long stop); // https://redis.io/commands/ZRank long? ZRank<T>(string cacheKey, T cacheValue); Task<long?> ZRankAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/ZRem long ZRem<T>(string cacheKey, IList<T> cacheValues); Task<long> ZRemAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/ZScore double? ZScore<T>(string cacheKey, T cacheValue); Task<double?> ZScoreAsync<T>(string cacheKey, T cacheValue);
- متدهای Hyperloglog
// https://redis.io/commands/PfAdd bool PfAdd<T>(string cacheKey, List<T> values); Task<bool> PfAddAsync<T>(string cacheKey, List<T> values); // https://redis.io/commands/PfCount long PfCount(List<string> cacheKeys); Task<long> PfCountAsync(List<string> cacheKeys); // https://redis.io/commands/PfMerge bool PfMerge(string destKey, List<string> sourceKeys); Task<bool> PfMergeAsync(string destKey, List<string> sourceKeys);
- متدهای Geo
// https://redis.io/commands/GeoAdd long GeoAdd(string cacheKey, List<(double longitude, double latitude, string member)> values); Task<long> GeoAddAsync(string cacheKey, List<(double longitude, double latitude, string member)> values); // https://redis.io/commands/GeoDist double? GeoDist(string cacheKey, string member1, string member2, string unit = "m"); Task<double?> GeoDistAsync(string cacheKey, string member1, string member2, string unit = "m"); // https://redis.io/commands/GeoHash List<string> GeoHash(string cacheKey, List<string> members); Task<List<string>> GeoHashAsync(string cacheKey, List<string> members); // https://redis.io/commands/GeoPos List<(decimal longitude, decimal latitude)?> GeoPos(string cacheKey, List<string> members); Task<List<(decimal longitude, decimal latitude)?>> GeoPosAsync(string cacheKey, List<string> members);
Install-Package EasyCaching.HybridCache Install-Package EasyCaching.InMemory Install-Package EasyCaching.Redis Install-Package EasyCaching.Bus.Redis
services.AddEasyCaching(option => // local option.UseInMemory("c1"); // distributed option.UseRedis(config => config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }, "c2"); // combine local and distributed option.UseHybrid(config => // specify the local cache provider name after v0.5.4 config.LocalCacheProviderName = "c1" // specify the distributed cache provider name after v0.5.4 config.DistributedCacheProviderName = "c2" }); // use redis bus .WithRedisBus(busConf => busConf.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
طراحی فرم ثبت نام کاربران در سایت با روش model driven
در این قسمت قصد داریم فرم ثبت نام کاربران را به همراه اعتبارسنجیهای پیشرفتهای پیاده سازی کنیم. به همین منظور، ابتدا پوشهی جدید App\users را به مثال سری جاری اضافه کنید و سپس سه فایل user.ts، signup-form.component.ts و signup-form.component.html را به آن اضافه نمائید.
فایل user.ts بیانگر مدل کاربران سایت است؛ با این محتوا:
export interface IUser { id: number; name: string; email: string; password: string; }
قالب فرم یا signup-form.component.html، در حالت ابتدایی آن چنین شکل استانداردی را خواهد داشت و فاقد اعتبارسنجی خاصی است:
<form> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" /> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" /> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" /> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
بنابراین ابتدا کلاس کامپوننت این فرم را در فایل signup-form.component.ts به نحو ذیل تکمیل کنید:
import { Component } from '@angular/core'; import { Control, ControlGroup, Validators } from '@angular/common'; @Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html' }) export class SignupFormComponent { form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) }); onSubmit(): void { console.log(this.form.value); } }
<form [ngFormModel]="form" (ngSubmit)="onSubmit()"> <div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name"/> <label class="text-danger" *ngIf="!form.controls['name'].valid"> Username is required. </label> </div> <div class="form-group"> <label form="email">Email</label> <input id="email" type="text" class="form-control" ngControl="email" #email="ngForm"/> <label class="text-danger" *ngIf="email.touched && !email.valid"> Email is required. </label> </div> <div class="form-group"> <label form="password">Password</label> <input id="password" type="password" class="form-control" ngControl="password" #password="ngForm"/> <label class="text-danger" *ngIf="password.touched && !password.valid"> Password is required. </label> </div> <button class="btn btn-primary" type="submit">Submit</button> </form>
تفاوت مهم این فرم و اعتبارسنجیهایش با قسمت قبل، در ایجاد اشیاء Control و ControlGroup به صورت صریح است:
form = new ControlGroup({ name: new Control('', Validators.required), email: new Control('', Validators.required), password: new Control('', Validators.required) });
import { Control, ControlGroup, Validators } from '@angular/common';
یک نکته
اگر محل قرارگیری کلاسی را فراموش کردید، آنرا در مستندات AngularJS 2.0 ذیل قسمت API Review جستجو کنید. نتیجهی جستجو، به همراه نام ماژول کلاسها نیز میباشد.
خاصیت عمومی form که با new ControlGroup تعریف شدهاست، حاوی تعاریف صریح کنترلهای موجود در فرم خواهد بود. در اینجا سازندهی ControlGroup، یک شیء را میپذیرد که کلیدهای آن، همان نام کنترلهای تعریف شدهی در قالب فرم و مقدار هر کدام، یک Control جدید است که پارامتر اول آن یک مقدار پیش فرض و پارامتر دوم، اعتبارسنجی مرتبطی را تعریف میکند (این اعتبارسنجی معرفی شده، یک متد استاتیک در کلاس توکار Validators است).
بنابراین چون سه المان ورودی، در فرم جاری تعریف شدهاند، سه کلید جدید هم نام نیز در پارامتر ورودی ControlGroup ذکر گردیدهاند.
اکنون که خاصیت عمومی form، در کلاس کامپوننت فوق تعریف شد، آنرا در قالب فرم، به ngFormModel بایند میکنیم:
<form [ngFormModel]="form" (ngSubmit)="onSubmit()">
مراحل بعد آن، با مراحلی که در قسمت قبل بررسی کردیم، تفاوتی ندارند:
الف) در اینجا به هر المان موجود، یک ngControl نسبت داده شدهاست تا هر المان را تبدیل به یک کنترل AngularJS2 2.0 کند.
ب) به هر المان، یک متغیر محلی شروع شده با # نسبت داده شدهاست تا با اتصال آن به ngForm بتوان به ngControl تعریف شده دسترسی پیدا کرد.
البته اکنون میتوان از خاصیت form متصل به ngFormModel نیز بجای تعریف این متغیر محلی، به نحو ذیل استفاده کرد:
<label class="text-danger" *ngIf="!form.controls['name'].valid">
د) و در آخر متد onSumbit موجود در کلاس کامپوننت را به رخداد ngSubmit متصل کردهایم. همانطور که ملاحظه میکنید اینبار دیگر پارامتری را به آن ارسال نکردهایم. از این جهت که خاصیت form موجود در سطح کلاس، اطلاعات کاملی را از اشیاء موجود در آن دارد و در متد onSubmit کلاس، به آن دسترسی داریم.
onSubmit(): void { console.log(this.form.value); }
بنابراین تا اینجا تنها تفاوت فرم جدید تعریف شده با قسمت قبل، تعریف صریح ControlGroup و کنترلهای آن در کلاس کامپوننت و اتصال آن به ngFormModel است. به این نوع فرمها، فرمهای model driven هم میگویند.
نمایش فرم افزودن کاربران توسط سیستم Routing
با نحوهی تعریف مسیریابیها در قسمت نهم آشنا شدیم. برای نمایش فرم افزودن کاربران، میتوان تغییرات ذیل را به فایل app.component.ts اعمال کرد:
//same as before... import { SignupFormComponent } from './users/signup-form.component'; @Component({ //same as before… template: ` //same as before… <li><a [routerLink]="['AddUser']">Add User</a></li> //same as before… `, //same as before… }) @RouteConfig([ //same as before… { path: '/adduser', name: 'AddUser', component: SignupFormComponent } ]) //same as before...
معرفی کلاس FormBuilder
روش دیگری نیز برای ساخت ControlGroup و کنترلهای آن با استفاده از کلاس و سرویس فرم ساز توکار AngularJS 2.0 وجود دارد:
import { Control, ControlGroup, Validators, FormBuilder } from '@angular/common'; form: ControlGroup; constructor(formBuilder: FormBuilder) { this.form = formBuilder.group({ name: ['', Validators.required], email: ['', Validators.required], password: ['', Validators.required] }); }
پیاده سازی اعتبارسنجی سفارشی
فرض کنید میخواهیم ورود نام کاربرهای دارای فاصله را غیر معتبر اعلام کنیم. برای این منظور فایل جدید usernameValidators.ts را به پوشهی app\users اضافه کنید؛ با این محتوا:
import { Control } from '@angular/common'; export class UsernameValidators { static cannotContainSpace(control: Control) { if (control.value.indexOf(' ') >= 0) { return { cannotContainSpace: true }; } return null; } }
هر متد پیاده سازی کنندهی یک اعتبار سنجی سفارشی در این کلاس، استاتیک تعریف میشود؛ با نام دلخواهی که مدنظر است.
پارامتر ورودی این متدهای استاتیک، یک وهله از شیء کنترل است که توسط آن میتوان برای مثال به خاصیت value آن دسترسی یافت و بر این اساس منطق اعتبارسنجی خود را پیاده سازی نمود. به همین جهت import آن نیز به ابتدای فایل جاری اضافه شدهاست.
خروجی این متد دو حالت دارد:
الف) اگر null باشد، یعنی اعتبارسنجی موفقیت آمیز بودهاست.
ب) اگر اعتبارسنجی با شکست مواجه شود، خروجی این متد یک شیء خواهد بود که کلید آن، نام اعتبارسنجی مدنظر است و مقدار این کلید، هر چیزی میتواند باشد؛ یک true و یا یک شیء دیگر که اطلاعات بیشتری را در مورد این شکست ارائه دهد.
برای مثال اگر اعتبارسنج توکار required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
پس از پیاده سازی یک اعتبارسنجی سفارشی، برای استفادهی از آن، ابتدا ماژول آنرا به ابتدای ماژول signup-form.component.ts اضافه میکنیم:
import { UsernameValidators } from './usernameValidators';
name: ['', Validators.compose([Validators.required, UsernameValidators.cannotContainSpace])],
و مرحلهی آخر، نمایش یک پیام اعتبارسنجی مناسب و متناظر با متد cannotContainSpace است. برای این منظور فایل signup-form.component.html را گشوده و تغییرات ذیل را اعمال کنید:
<div class="form-group"> <label form="name">Username</label> <input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> </div> </div>
نام خاصیت بازگشت داده شدهی در اعتبارسنجی سفارشی، به عنوان یک خاصیت جدید شیء errors قابل استفاده است؛ مانند name.errors.cannotContainSpace.
به عنوان تمرین ماژول جدید emailValidators.ts را افزوده و سپس اعتبارسنجی سفارشی بررسی معتبر بودن ایمیل وارد شده را تعریف کنید:
import {Control} from '@angular/common'; export class EmailValidators { static email(control: Control) { var regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; var valid = regEx.test(control.value); return valid ? null : { email: true }; } }
یک نکته
اگر نیاز است از regular expressions مانند مثال فوق استفاده شود، میتوان از متد توکار Validators.pattern نیز استفاده کرد و نیازی به تعریف یک متد جداگانه برای آن وجود ندارد؛ مگر اینکه نیاز به بازگشت شیء خطای کاملتری با خواص بیشتری وجود داشته باشد.
اعتبارسنجی async یا اعتبارسنجی از راه دور (remote validation)
یک سری از اعتبارسنجیها را در سمت کلاینت میتوان تکمیل کرد؛ مانند بررسی معتبر بودن فرمت ایمیل وارده. اما تعدادی دیگر، نیاز به اطلاعاتی از سمت سرور دارند. برای مثال آیا نام کاربری در حال ثبت، تکراری است یا خیر؟ این نوع اعتبارسنجیها در ردهی async validation قرار میگیرند.
سازندهی شیء Control در AngularJS 2.0 که در مثالهای بالا نیز مورد استفاده قرار گرفت، پارامتر اختیاری سومی را نیز دارد که یک AsyncValidatorFn را قبول میکند:
control(value: Object, validator?: ValidatorFn, asyncValidator?: AsyncValidatorFn) : Control
nameShouldBeUnique(control: Control) { let name: string = control.value; return new Promise((resolve) => { this._userService.isUserNameUnique(<IUser>{ "name": name }).subscribe( (result: IResult) => { resolve( result.result ? null : { 'nameShouldBeUnique': true } ); }, error => { resolve(null); } ); }); }
this.form = _formBuilder.group({ name: ['', Validators.compose([ Validators.required, UsernameValidators.cannotContainSpace ]), (control: Control) => this.nameShouldBeUnique(control)],
امضای متد nameShouldBeUnique تفاوتی با سایر متدهای اعتبارسنج نداشته و پارامتر ورودی آن، همان کنترل است که توسط آن میتوان به مقدار وارد شدهی توسط کاربر دسترسی یافت. اما تفاوت اصلی آن در اینجا است که این متد باید یک شیء Promise را بازگشت دهد. یک Promise، بیانگر نتیجهی یک عملیات async است. در اینجا دو حالت resolve و error را باید پیاده سازی کرد. در حالت error، یعنی عملیات async صورت گرفته با شکست مواجه شدهاست و در حالت resolve، یعنی عملیات تکمیل شده و اکنون میخواهیم نتیجهی نهایی را بازگشت دهیم. نتیجه نهایی بازگشت داده شدهی در اینجا، همانند سایر validators است و اگر نال باشد، یعنی اعتبارسنجی موفقیت آمیز بوده و اگر یک شیء را بازگشت دهیم، یعنی اعتبارسنجی با شکست مواجه شدهاست.
این Promise، از یک سرویس تعریف شدهی در فایل جدید user.service.ts استفاده میکند:
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _checkUserUrl = '/home/checkUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } isUserNameUnique(user: IUser): Observable<IResult> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._checkUserUrl, JSON.stringify(user), options) .map((response: Response) => <IResult>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
export interface IResult { result: boolean; }
کدهای سمت سرور برنامه که کار بررسی یکتا بودن نام کاربری را انجام میدهند، به صورت ذیل در فایل Controllers\HomeController.cs تعریف شدهاند:
namespace MVC5Angular2.Controllers { public class HomeController : Controller { [HttpPost] public ActionResult CheckUser(User user) { var isUnique = new { result = true }; if (user.Name?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false) { isUnique = new { result = false }; } return new ContentResult { Content = JsonConvert.SerializeObject(isUnique, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
بنابراین تا اینجا مسیر سمت سرور home/checkuser تکمیل شدهاست. این مسیر توسط سرویس کاربر صدا زده شده و اگر نام کاربری وارد شده موجود باشد، نتیجهای را مطابق امضای قرارداد IResult سفارشی ما بازگشت میدهد.
پس از آن مجددا به فایل signup-form.component.ts مراجعه کرده و سرویس جدید UserService را به سازندهی آن تزریق کردهایم. همچنین قسمت providers این کامپوننت را هم جهت تکمیل اطلاعات تزریق کنندهی توکار AngularJS 2.0 مقدار دهی کردهایم. البته همانطور که در مبحث تزریق وابستگیها نیز عنوان شد، اگر این سرویس قرار نیست در کلاس دیگری استفاده شود، نیازی نیست تا آنرا در بالاترین سطح ممکن و در فایل app.component.ts ثبت و معرفی کرد:
@Component({ selector: 'signup-form', templateUrl: 'app/users/signup-form.component.html', providers: [ UserService ] }) export class SignupFormComponent { constructor(private _formBuilder: FormBuilder, private _userService: UserService) {
اکنون که کدهای فعال سازی اعتبارسنجی از راه دور ما تکمیل شدهاست، به فایل signup-form.component.html مراجعه کرده و پیام مناسبی را نمایش خواهیم داد:
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.required"> Username is required. </label> <label class="text-danger" *ngIf="name.errors.cannotContainSpace"> Username can't contain space. </label> <label class="text-danger" *ngIf="name.errors.nameShouldBeUnique"> This username is already taken. </label> </div>
نمایش پیام loading در حین انجام اعتبارسنجی از راه دور
شاید بد نباشد که در حین انجام عملیات اعتبارسنجی از راه دور و ارسال درخواستی به سرور و بازگشت نتیجهی آن، یک پیام loading را نیز نمایش داد. برای انجام اینکار نیاز است تغییرات ذیل را به فایل signup-form.component.html اضافه کنیم:
<input id="name" type="text" class="form-control" ngControl="name" #name="ngForm" /> <div *ngIf="name.control.pending"> Checking server, Please wait ... </div>
اعتبارسنجی ترکیبی در حین submit یک فرم
فرض کنید میخواهید منطقی را که حاصل اعتبارسنجی تمام فیلدهای فرم است (و نه هر کدام به تنهایی)، در حین submit آن اعمال کنید. برای مثال آیا ترکیب نام کاربری و کلمهی عبور شخصی در حین login معتبر است یا خیر؟ در این حالت پس از بررسیهای لازم در متد onSubmit، میتوان با استفاده از متد find شیء form، به یکی از کنترلهای فرم دسترسی یافت و سپس با استفاده از متد setErrors، خطای اعتبارسنجی سفارشی را به آن اضافه کرد:
onSubmit(): void { console.log(this.form.value); this.form.find('name').setErrors({ invalidData : true }); }
<div *ngIf="name.touched && name.errors"> <label class="text-danger" *ngIf="name.errors.invalidData"> Check the inputs.... </label> </div>
اتصال المانهای فرم به مدلی جهت ارسال به سرور
اکنون که دسترسی به خاصیت this.form را داریم و این خاصیت توسط [ngFormModel] به تمام اشیاء تعریف شدهی در فرم و تغییرات آنها دسترسی دارد، میتوان از آن برای دسترسی به شیءایی که حاوی مدل فرم است، استفاده کرد. برای نمونه در مثال فوق، خاصیت value آن، چنین خروجی را دارد:
{ name="VahidN", email="email@site.com", password="123"}
import { Injectable } from '@angular/core'; import { Http, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import { Headers, RequestOptions } from '@angular/http'; import { IUser } from './user'; import { IResult } from './result'; @Injectable() export class UserService { private _addUserUrl = '/home/addUser'; constructor(private _http: Http) { } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } addUser(user: IUser): Observable<IUser> { let headers = new Headers({ 'Content-Type': 'application/json' }); // for ASP.NET MVC let options = new RequestOptions({ headers: headers }); return this._http.post(this._addUserUrl, JSON.stringify(user), options) .map((response: Response) => <IUser>response.json()) .do(data => console.log("User: " + JSON.stringify(data))) .catch(this.handleError); } }
[HttpPost] public ActionResult AddUser(User user) { user.Id = 1; //todo: save user and get id from db return new ContentResult { Content = JsonConvert.SerializeObject(user, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; }
onSubmit(): void { console.log(this.form.value); /*this.form.find('name').setErrors({ invalidData : true });*/ this._userService.addUser(<IUser>this.form.value) .subscribe((user: IUser) => { console.log(`ID: ${user.id}`); }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: (این کدها مطابق نگارش RC 1 هستند)
MVC5Angular2.part11.zip
خلاصهی بحث
برای اینکه بتوان کنترل بیشتری را بر روی المانهای فرم داشت، ابتدا سرویس FormBuilder را در سازندهی کلاس کامپوننت فرم تزریق میکنیم. سپس با استفاده از متد group آن، المانهای فرم را به صورت کلیدهای شیء پارامتر آن تعریف میکنیم. در اینجا میتوان اعتبارسنجیهای توکار AngularJS 2.0 را که در کلاس پایهی Validators مانند Validators.required وجود دارند، تعریف کرد. با استفاده از متد compose آنها را ترکیب نمود و یا پارامتر سومی را جهت اعتبارسنجیهای async اضافه نمود. در این حالت شیء form تعریف شده به صورت [ngFormModel] به قالب فرم متصل میشود و از تغییرات آن آگاه خواهد شد.
اضافه کردن دکمهی More info به صفحهی About و مدیریت کلیک بر روی آن
فایل Scripts\Templates\about.hbs را گشوده و سپس محتوای فعلی آن را به نحو ذیل تکمیل کنید:
<h2>About Ember Blog</h2> <p>Bla bla bla!</p> <button class="btn btn-primary" {{action 'showRealName' }}>more info</button>
به همین جهت فایل جدید Scripts\Controllers\about.js را در پوشهی کنترلرهای سمت کاربر اضافه کنید (نام آن با نام مسیریابی یکی است)؛ با این محتوا:
Blogger.AboutController = Ember.Controller.extend({ actions: { showRealName: function () { alert("You clicked at showRealName of AboutController."); } } });
در ادامه این خاصیت را با تهیه یک زیرکلاس از کلاس پایه Controller تهیه شده توسط ember.js مقدار دهی میکنیم. به این ترتیب به کلیه امکانات این کلاس پایه دسترسی خواهیم داشت؛ به علاوه میتوان ویژگیهای سفارشی را نیز به آن افزود. برای مثال در اینجا در قسمت actions آن، دقیقا مطابق نام اکشنی که در فایل about.hbs تعریف کردهایم، یک متد جدید اضافه شدهاست.
پس از تعریف کنترلر about.js نیاز است مدخل متناظر با آنرا به فایل index.html برنامه نیز در انتهای تعاریف موجود، اضافه کرد:
<script src="Scripts/Controllers/about.js" type="text/javascript"></script>
اکنون یکبار برنامه را اجرا کرده و در صفحهی about بر روی دکمهی more info کلیک کنید.
اضافه کردن دکمهی ارسال پیام خصوصی به صفحهی Contact و مدیریت کلیک بر روی آن
در ادامه به قالب فعلی Scripts\Templates\contact.hbs یک دکمه را جهت ارسال پیام خصوصی اضافه میکنیم.
<h1>Contact</h1> <div class="row"> <div class="col-md-6"> <p> Want to get in touch? <ul> <li>{{#link-to 'phone'}}Phone{{/link-to}}</li> <li>{{#link-to 'email'}}Email{{/link-to}}</li> </ul> </p> <p> Or, click here to send a secret message: </p> <button class="btn btn-primary" {{action 'sendMessage' }}>Send message</button> </div> <div class="col-md-6"> {{outlet}} </div> </div>
Blogger.ContactController = Ember.Controller.extend({ actions: { sendMessage: function () { var message = prompt('Type your message here:'); } } });
<script src="Scripts/Controllers/contact.js" type="text/javascript"></script>
نمایش تصویری تعاملی در صفحهی about
تا اینجا با نحوهی تعریف اکشنها در قالبها و مدیریت آنها توسط کنترلرهای متناظر آشنا شدیم. در ادامه قصد داریم با اصول binding اطلاعات در ember.js آشنا شویم. برای مثال فرض کنید میخواهیم دکمهای را در صفحهی about قرار داده و با کلیک بر روی آن، لوگوی ember.js را که به صورت یک تصویر مخفی در صفحه قرار دارد، نمایان کنیم. برای اینکار نیاز است خاصیتی را در کنترلر متناظر، تعریف کرده و سپس آنرا به template جاری bind کرد.
برای این منظور فایل Scripts\Templates\about.hbs را گشوده و تعاریف موجود آنرا به نحو ذیل تکمیل کنید:
<h2>About Ember Blog</h2> <p>Bla bla bla!</p> <button class="btn btn-primary" {{action 'showRealName' }}>more info</button> {{#if isAuthorShowing}} <button class="btn btn-warning" {{action 'hideAuthor' }}>Hide Image</button> <p><img src="Content/images/ember-productivity-sm.png"></p> {{else}} <button class="btn btn-info" {{action 'showAuthor' }}>Show Image</button> {{/if}}
کنترلر about (فایل Scripts\Controllers\about.js) جهت مدیریت این خاصیت جدید، به همراه دو اکشن تعریف شده، اینبار به نحو ذیل تغییر خواهد یافت:
Blogger.AboutController = Ember.Controller.extend({ isAuthorShowing: false, actions: { showRealName: function () { alert("You clicked at showRealName of AboutController."); }, showAuthor: function () { this.set('isAuthorShowing', true); }, hideAuthor: function () { this.set('isAuthorShowing', false); } } });
سپس در دو متد showAuthor و hideAuthor که به اکشنهای دو دکمهی جدید تعریف شده در قالب about متصل خواهند شد، نحوهی تغییر مقدار خاصیت isAuthorShowing را توسط متد set ملاحظه میکنید.
این قسمت مهمترین تفاوت ember.js با jQuery است. در jQuery مستقیما المانهای صفحه در همانجا تغییر داده میشوند. در ember.js منطق مدیریت کنندهی رابط کاربری و کدهای قالب متناظر با آن از هم جدا شدهاند تا بتوان یک برنامهی بزرگ را بهتر مدیریت کرد. همچنین در اینجا مشخص است که هر قسمت و هر فایل، چه ارتباطی با سایر اجزای تعریف شده دارد و چگونه به هم متصل شدهاند و اینبار شاهد انبوهی از کدهای جاوا اسکریپتی مخلوط بین المانهای HTML صفحه نیستیم.
نمایش پیامی به کاربر پس از ارسال پیام خصوصی در صفحهی تماس با ما
قصد داریم ویژگی مشابهی را به صفحهی contact نیز اضافه کنیم. اگر کاربر بر روی دکمهی ارسال پیام کلیک کرد، پیام تشکری به همراه عددی ویژه به او نمایش خواهیم داد.
برای اینکار قالب Scripts\Templates\contact.hbs را به نحو ذیل تکمیل کنید:
<h1>Contact</h1> <div class="row"> <div class="col-md-6"> <p> Want to get in touch? <ul> <li>{{#link-to 'phone'}}Phone{{/link-to}}</li> <li>{{#link-to 'email'}}Email{{/link-to}}</li> </ul> </p> {{#if messageSent}} <p> Thank you. Your message has been sent. Your confirmation number is {{confirmationNumber}}. </p> {{else}} <p> Or, click here to send a secret message: </p> <button class="btn btn-primary" {{action 'sendMessage' }}>Send message</button> {{/if}} </div> <div class="col-md-6"> {{outlet}} </div> </div>
برای تعریف منطق مرتبط با این خواص، به کنترلر contact واقع در فایل Scripts\Controllers\contact.js مراجعه کرده و آنرا به نحو ذیل تغییر میدهیم:
Blogger.ContactController = Ember.Controller.extend({ messageSent: false, actions: { sendMessage: function () { var message = prompt('Type your message here:'); if (message) { this.set('confirmationNumber', Math.round(Math.random() * 100000)); this.set('messageSent', true); } } } });
بنابراین به صورت خلاصه، کار کنترلر، مدیریت منطق نمایشی برنامه است و برای اینکار حداقل دو مکانیزم را ارائه میدهد: اکشنها و خواص. اکشنها بیانگر نوعی رفتار هستند؛ برای مثال نمایش یک popup و یا تغییر مقدار یک خاصیت. مقدار خواص را میتوان مستقیما در صفحه نمایش داد و یا از آنها جهت پردازش عبارات شرطی و نمایش قسمت خاصی از قالب جاری نیز میتوان کمک گرفت.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_02.zip
- آدرسهای مجازی به صورت پیوسته و پشت سر هم هستند و آدرس دهی بسیار راحت است ولی دادهها بر روی یک حافظه به صورت متصل به هم یا پیوسته ذخیره یا خوانده نمیشوند و کار آدرس دهی مشکل است. پس یکی از مزایای داشتن آدرس دهی مجازی پشت سر هم قرار گرفتن آدرس هاست.
- برنامه از آدرسهای مجازی برای دسترسی به بافر حافظه استفاده میکند که بزرگتر از حافظه فیزیکی موجود هست. موقعی که نیاز به حافظه بیشتر باشد و حافظه سیستم کوچکتر یا کمتر از تقاضا باشد، مدیر حافظه، صفحات حافظه فیزیکی را به صورت یک فایل (عموما 4 کیلیویی) بر روی دیسک سخت ذخیره میکند و صفحات دادهها در موقع نیاز بین حافظه فیزیکی و دیسک سخت جابجا میشود.
- هر پردازشی که بر روی آدرسهای مجازی کار میکند ایزوله شده است. یعنی یک پروسه هیچ گاه نمیتواند به آدرسهای یک پروسه دیگر دسترسی داشته باشد و باعث تخریب دادههای آن شود.
در شکل بالا دو پروسه 64 بیتی به نامهای notepad.exe و myapp.exe قرار دارند که هر کدام فضای آدرسهای مجازی خودشان را دارند و از آدرس 0x000'0000000 شروع و تا آدرس 0x7FF'FFFFFFFF ادامه میابند. هر قسمت شامل یک صفحه 4 کیلویی از حافظه مجازی یا فیزیکی است. به برنامه نوتپد دقت کنید که از سه صفحه پشت سر هم یا پیوسته تشکیل شده که آدرس شروع آن 0x7F7'93950000 می باشد ولی در حافظه فیزیکی خبری از پیوسته بودن دیده نمیشود و حتما این نکته را متوجه شدید که هر دو پروسه از یک آدرس شروع استفاده کردهاند، ولی به آدرسی متفاوت از حافظه فیزیکی نگاشت شده اند.
فضای کاربری و فضای سیستمی User space and system space
گفتیم بسیاری از پروسهها در حالت user mode و پروسههای هسته سیستم عامل و درایورها در حالت kernel mode اجرا میشوند. هر پروسه مد کاربر از فضای آدرس دهی مجازی خودش استفاده میکند ولی در حالت کرنل همه از یک فضای آدرس دهی استفاده میکنند که به آن فضای سیستمی میگویند و برای مد کاربری میگویند فضای کاربری.
در سیستمهای 32 بیتی نهایتا تا 4 گیگ حافظه میتوان به اینها تخصیص داد؛ 2 گیگ ابتدایی به user space و دو گیگ بعدی به system space :
در ویندوزهای 32 بیتی شما امکان تغییر این مقدار حافظه را در میان بوت دارید و میتوانید حافظه کاربری را تا 3 گیگ مشخص کنید و یک گیگ را برای فضای سیستمی. برای اینکار میتوانید از برنامه bcedit استفاده کنید.
در سیستمهای 64 بیتی میزان حافظههای مجازی به صورت تئوری تا 16 اگزابایت مشخص شده است؛ ولی در عمل تنها بخش کوچکی از آن یعنی 8 ترابایت استفاده میشود.
- برنامه جهت اجرا در مد کاربر یک درخواست را برای خواندن دادههای یک device را آماده میکند. سپس برنامه آدرس شروع یک بافر را برای دریافت داده، مشخص میکند.
- وظیفه این درایور یک قطعه در مد کرنل این است که عملیات خواندن را شروع کرده و کنترل را به درخواست کننده ارسال میکند.
- بعد device یک وقفه را به هر تردی thread که در حال اجراست ارسال میکند تا بگوید، عملیات خواندن پایان یافته است. این وقفه توسط ترد درایور مربوطه دریافت میشود.
- حالا دیگر درایور نباید دادهها را در همان جایی که گام اول برنامه مشخص کرده است ذخیره کند. چون این آدرس که برنامه در مد کاربری مشخص کرده است، با نمونهای که این فرآیند محاسبه میکند متفاوت است.
مقابله با XSS ؛ یکبار برای همیشه!
public class Contact { public int Id { get; set; } [AllowHtml] public string Name { get; set; } public string Address { get; set; } }
[HttpGet] public ActionResult ContactUs() { return View(); } [HttpPost] public ActionResult ContactUs(Contact con) { ViewBag.Name = con.Name.ToSafeHtml(); ViewBag.MyAddress = con.Address; return View(); }
A potentially dangerous Request.Form value was detected from the client (Name="<script>alert("aaa")...").
نصب پرباد و انجام تنظیمات اولیهی آن
بستههای نیوگت پرباد را در دو پروژهی زیر نصب خواهیم کرد:
الف) پروژهی Web API (و یا همان BlazorWasm.WebApi در مثال این سری):
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Parbad.AspNetCore" Version="1.1.0" /> <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" /> </ItemGroup> </Project>
ب) پروژهای که محل قرارگیری فایلهای Migration است (و یا همان BlazorServer.DataAccess) در این مثال:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Parbad.Storage.EntityFrameworkCore" Version="1.2.0" /> </ItemGroup> </Project>
پس از نصب این بستهها، به کلاس آغازین پروژهی Web API مراجعه کرده و تنظیمات سرویسها و همچنین میانافزار پرباد را انجام میدهیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... var connectionString = Configuration.GetConnectionString("DefaultConnection"); services.AddParbad() .ConfigureHttpContext(httpContextBuilder => httpContextBuilder.UseDefaultAspNetCore()) .ConfigureGateways(gatewayBuilder => { gatewayBuilder .AddParbadVirtual() .WithOptions(gatewayOptions => gatewayOptions.GatewayPath = "/MyVirtualGateway"); }) .ConfigureStorage(storageBuilder => { storageBuilder.UseEfCore(efCoreOptions => { var assemblyName = typeof(ApplicationDbContext).Assembly.GetName().Name; efCoreOptions.ConfigureDbContext = db => db.UseSqlServer( connectionString, sqlServerOptionsAction: sqlOptions => sqlOptions.MigrationsAssembly(assemblyName) ); }); }) .ConfigureAutoTrackingNumber(opt => opt.MinimumValue = 1) .ConfigureOptions(parbadOptions => { // parbadOptions.Messages.PaymentSucceed = "YOUR MESSAGE"; }); // ... } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); if (env.IsDevelopment()) { app.UseParbadVirtualGatewayWhenDeveloping(); } else { app.UseParbadVirtualGateway(); } } } }
- در متد ConfigureGateways میتوان چندین درگاه را معرفی کرد که برای مثال در اینجا از درگاه مجازی و محلی آن استفاده شدهاست.
- در متد ConfigureStorage، تنظیمات EF-Core آنرا مشاهده میکنید. پرباد به همراه DbContext خاص خودش است. یعنی در این حالت برنامهی شما حداقل دو DbContext خواهد داشت؛ یکی ApplicationDbContext و دیگری ParbadDataContext.
- میخواهیم شمارهی تراکنشها را به صورت خودکار توسط پرباد مدیریت کنیم. به همین جهت میتوان عدد ابتدای آنرا توسط متد ConfigureAutoTrackingNumber مشخص کرد.
- در پایان هم تعاریف مسیریابی میانافزار آنرا مشاهده میکنید که میتواند برای حالت توسعه و ارائهی نهایی متفاوت باشد.
تکمیل خواص موجودیت RoomOrderDetail جهت کار با پرباد
موجودیت RoomOrderDetail را در قسمت قبل معرفی کردیم. پرباد به ازای هر تراکنش بانکی که صورت میگیرد، یا نیاز به یک TrackingNumber خودکار را دارد و یا دستی. یعنی یا میتوانیم شماره تراکنش خاص خودمان را تولید کنیم و در اختیار آن قرار دهیم و یا از آن درخواست کنیم تا این شماره را مدیریت کرده و به صورت خودکار تولید کند. در هر دو حالت نیاز است این شماره را به ردیفهای جدول جزئیات سفارشات اتاقهای هتل اضافه کرد که در این مثال ParbadTrackingNumber نام دارد:
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlazorServer.Entities { public class RoomOrderDetail { // ... [Required] public long ParbadTrackingNumber { get; set; } public bool IsPaymentSuccessful { get; set; } public string Status { get; set; } } }
ایجاد جداول متناظر با ParbadDataContext
همانطور که عنوان شد، اکنون برنامه به همراه دو DbContext است. بنابراین در این حالت در حین اجرای مهاجرتها، ذکر نام Context مدنظر اجباری است.
برای ایجاد مهاجرتهای متناظر با ParbadDataContext، از طریق خط فرمان به پوشهی BlazorServer.DataAccess وارد شده و دستورات زیر را اجرا میکنیم:
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbadFields --context ApplicationDbContext dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddParbad --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context Parbad.Storage.EntityFrameworkCore.Context.ParbadDataContext
روش یکپارچه سازی پرباد با یک برنامهی SPA
روش متداول کار با پرباد، بر اساس طراحی مخصوص ASP.NET Core آن است. ابتدا درخواستی را به آن ارسال میکنید. سپس پرباد شماره تراکنشی را تولید کرده و شروع تراکنش را در بانک اطلاعاتی ثبت میکند. در ادامه به صورت خودکار، کار ارسال اطلاعات به درگاه بانکی (برای مثال ارسال تمام فیلدهای یک فرم ویژهی آن بانک، بر اساس مستندات آن) و هدایت به درگاه بانکی را انجام میدهد. پس از پایان کار پرداخت، کار هدایت به اکشن متد دریافت تائیدیهی نهایی صورت میگیرد و همینجا کار به پایان میرسد. این روش هرچند برای برنامههای سمت سرور ASP.NET Core کار میکند، اما ... به همین نحو با برنامههای تک صفحهای وب مانند Blazor WASM قابل استفاده نیست. در اینجا روش تبادل اطلاعات با اکشن متدهای وب سرویسهای برنامه از طریق یک HttpClient است و در این حالت دیگر نمیتوان از مزایای Post و Redirect خودکار پرباد که در سمت سرور صورت میگیرد استفاده کرد. با استفاده از HttpClient، یک شیء را به سمت Web API ارسال میکنیم و در پاسخ، فقط یک شیء را دریافت میکنیم. در اینجا دیگر خبری از Redirect به درگاه اصلی بانکی و Post اطلاعات به آن نیست. بنابراین روش کار با پرباد در اینجا به صورت زیر خواهد بود:
الف) شماره Id سفارش و مبلغ نهایی آنرا از طریق یک درخواست Get معمولی به اکشن متدی در سمت سرور ارسال میکنیم. یعنی نیاز است ابتدا Url زیر را تشکیل داد که شماره سفارش و مبلغ آن، به صورت کوئری استرینگهایی به اکشن متد PayRoomOrder ارسال میشوند:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
ب) اکنون چون یک redirect سمت سرور صورت گرفته، به صورت معمولی در اکشن متد PayRoomOrder با پرباد پردازش صورت گرفته و به سمت درگاه هدایت میشویم. پس از پرداخت نهایی، باز هم به صورت خودکار به اکشن متد دیگری جهت تائید عملیات هدایت خواهیم شد.
ج) در پایان کار، اکشن متد سمت سرور، ما را به سمت کامپوننتی در برنامهی کلاینت Redirect خواهد کرد:
https://localhost:5002/payment-result/OrderId/TrackingNumber/Message
بنابراین روش یکپارچه سازی پربابد با برنامههای SPA، بر اساس Redirectهای کامل است که سبب بارگذاری مجدد کل صفحه و آدرسها میشوند و در اینجا از HttpClient برای کار با پرباد استفاده نخواهیم کرد؛ چون تمام اعمال خودکار آنرا از دست خواهیم داد و مجبور به بازنویسی آنها خواهیم شد که در دراز مدت با تغییرات این کتابخانه، قابل نگهداری نخواهند بود. بنابراین بهتر است خود پرباد کار Redirectها و ارسال اطلاعات به درگاههای بانکی را مدیریت کند و نه ما از طریق کار با یک HttpClient.
آشنایی با گردش کار برنامه
در این مثال، مراحل زیر را طی خواهیم کرد:
1- شروع به انتخاب یک بازهی زمانی و تعداد شب اقامت
2- انتخاب یک اتاق از لیست اتاقها با کلیک بر روی دکمهی Book آن
3- کلیک بر روی دکمهی checkout، در صفحهی مشاهدهی جزئیات اتاق و شروع به پرداخت
4- هدایت به درگاه مجازی پرباد در سمت برنامهی Web API
5- پرداخت و هدایت خودکار به سمت برنامهی Web API، جهت تائید نهایی
6- هدایت نهایی به سمت برنامهی کلاینت، جهت نمایش اطلاعات پرداخت
ایجاد کنترلر پرداخت، توسط درگاه مجازی پرباد
پس از آشنایی با گردش کاری اطلاعات در اینجا، نیاز است بتوان لینک زیر را در برنامهی کلاینت تولید کرد و سپس کاربر را به سمت اکشن متد PayRoomOrder هدایت نمود:
https://localhost:5001/api/ParbadPayment/PayRoomOrder?orderId=1&amount=1000
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] public class ParbadPaymentController : Controller { private readonly IConfiguration _configuration; private readonly IOnlinePayment _onlinePayment; private readonly IRoomOrderDetailsService _roomOrderService; public ParbadPaymentController( IConfiguration configuration, IOnlinePayment onlinePayment, IRoomOrderDetailsService roomOrderService) { _configuration = configuration; _onlinePayment = onlinePayment ?? throw new ArgumentNullException(nameof(onlinePayment)); _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService)); } [HttpGet] public async Task<IActionResult> PayRoomOrder(int orderId, long amount) { var verifyUrl = Url.Action( action: nameof(ParbadPaymentController.VerifyRoomOrderPayment), controller: nameof(ParbadPaymentController).Replace("Controller", string.Empty), values: null, protocol: Request.Scheme); var result = await _onlinePayment.RequestAsync(invoiceBuilder => invoiceBuilder.UseAutoIncrementTrackingNumber() .SetAmount(amount) .SetCallbackUrl(verifyUrl) .UseParbadVirtual() ); if (result.IsSucceed) { await _roomOrderService.UpdateRoomOrderTrackingNumberAsync(orderId, result.TrackingNumber); // It will redirect the client to the gateway. return result.GatewayTransporter.TransportToGateway(); } else { return Redirect(getClientReturnUrl(orderId, result.TrackingNumber, result.Message)); } } [HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment() { var invoice = await _onlinePayment.FetchAsync(); var orderDetail = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(invoice.TrackingNumber); if (invoice.Status == PaymentFetchResultStatus.AlreadyProcessed) { return Redirect(getClientReturnUrl(orderDetail.Id, invoice.TrackingNumber, "The payment is already processed.")); } var verifyResult = await _onlinePayment.VerifyAsync(invoice); if (verifyResult.Status == PaymentVerifyResultStatus.Succeed) { var result = await _roomOrderService.MarkPaymentSuccessfulAsync(verifyResult.TrackingNumber, verifyResult.Amount); if (result == null) { return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, "Can not mark payment as successful")); } return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message)); } return Redirect(getClientReturnUrl(orderDetail.Id, verifyResult.TrackingNumber, verifyResult.Message)); } private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage) { var clientBaseUrl = _configuration.GetValue<string>("Client_URL"); return new Uri(new Uri(clientBaseUrl), $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString(); } } }
در اینجا کدهای کامل ParbadPaymentController مشاهده میکنید.
- گردش کاری پرداخت، با فراخوانی اکشن متد PayRoomOrder شروع میشود که دو پارامتر شماره سفارش و مبلغ آنرا دریافت میکند.
[HttpGet] public async Task<IActionResult> PayRoomOrder(int orderId, long amount)
- در اکشن متد PayRoomOrder، نیاز است لینک بازگشت از درگاه بانکی را مشخص کنیم. پس از اینکه کاربر پرداختی را انجام داد، مجددا به صورت خودکار، به سمت آدرسی در همین Web API و نه برنامهی سمت کلاینت هدایت میشود؛ چون هنوز کار پرباد به پایان نرسیده و باید عملیات انجام شده را تصدیق کند. به همین جهت ابتدا آدرس اکشن متدی که کار تائید نهایی را انجام میدهد، تولید کرده و به متد RequestAsync آن به همراه مبلغ نهایی و نوع درگاه، ارسال میکنیم.
- استفاده از UseAutoIncrementTrackingNumber سبب میشود تا پرباد خودش مدیریت TrackingNumber را انجام دهد که پس از پایان عملیات، توسط خاصیت result.TrackingNumber در دسترس خواهد بود.
- پس از پایان عملیات ابتدایی RequestAsync که سشن پرباد را ایجاد کرده و همچنین رکوردی را در بانک اطلاعاتی نیز ثبت میکند (در جداول درونی خود پرباد)، نیاز است رکورد سفارشی را که با آن کار را شروع کردیم یافته و TrackingNumber آنرا با مقدار واقعی دریافتی از پرباد، به روز رسانی کنیم. اینکار توسط متد UpdateRoomOrderTrackingNumberAsync انجام میشود:
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task UpdateRoomOrderTrackingNumberAsync(int roomOrderId, long trackingNumber) { var order = await _dbContext.RoomOrderDetails.FindAsync(roomOrderId); if (order == null) { return; } order.ParbadTrackingNumber = trackingNumber; _dbContext.RoomOrderDetails.Update(order); await _dbContext.SaveChangesAsync(); } } }
- اکنون با فراخوانی متد ()result.GatewayTransporter.TransportToGateway، دو کار مهم رخ میدهند:
الف) ارسال خودکار اطلاعات به سمت درگاه بانکی
ب) Redirect خودکار به سمت درگاه بانگی
به همین جهت است که علاقمند نبودیم تا این مراحل را توسط HttpClient برنامهی Blazor WASM مدیریت و بازنویسی کنیم.
- پس از هدایت به سمت درگاه بانکی و تکمیل پرداخت، اکنون مجددا به همان verifyUrl هدایت میشویم. یعنی اکنون به مرحلهی پردازش اکشن متد VerifyRoomOrderPayment در سمت Web API رسیدهایم.
[HttpGet, HttpPost] public async Task<IActionResult> VerifyRoomOrderPayment()
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } } }
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> MarkPaymentSuccessfulAsync(long trackingNumber, long amount) { var order = await _dbContext.RoomOrderDetails.FirstOrDefaultAsync(x => x.ParbadTrackingNumber == trackingNumber); if (order?.IsPaymentSuccessful != false || order.TotalCost != amount) { return null; } order.IsPaymentSuccessful = true; order.Status = BookingStatus.Booked; var markPaymentSuccessful = _dbContext.RoomOrderDetails.Update(order); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(markPaymentSuccessful.Entity); } } }
- در تمام این مراحل، کار Redirect به سمت کلاینت و کامپوننت payment-result آن، با فراخوانی متد return Redirect اکشن متدها صورت میگیرد که Url آن به صورت زیر تامین میشود:
private string getClientReturnUrl(int orderId, long trackingNumber, string errorMessage) { var clientBaseUrl = _configuration.GetValue<string>("Client_URL"); return new Uri(new Uri(clientBaseUrl), $"/payment-result/{orderId}/{trackingNumber}/{WebUtility.UrlEncode(errorMessage)}").ToString(); }
{ "Client_URL": "https://localhost:5002/" }
تکمیل قسمت سمت کلاینت عملیات پرداخت بانکی، توسط درگاه مجازی پرباد
تا اینجا کنترلری که کار پرداخت آنلاین را مدیریت میکند، پیاده سازی کردیم. قسمت آخر این بحث به تکمیل جزئیات این گردش کاری که شامل شروع عملیات سفارش و پرداخت از سمت کلاینت و نمایش پیام خطا یا موفقیت پرداخت به کاربر است، اختصاص دارد.
الف) تکمیل کامپوننت RoomDetails.razor جهت شروع به پرداخت آنلاین
کامپوننت RoomDetails.razor را در قسمت قبل آغاز کردیم و توسعهی آنرا تا جائی پیش بردیم که اعتبارسنجیهای آنرا به علت استفادهی از خواص تو در تو، به صورت دستی انجام دادیم. پس از مرحلهی اعتبارسنجی، اکنون میخواهیم کاربر را به سمت درگاه بانکی جهت پرداخت، هدایت کنیم:
@page "/hotel-room-details/{Id:int}" @inject IJSRuntime JsRuntime @inject ILocalStorageService LocalStorage @inject IClientHotelRoomService HotelRoomService @inject IClientRoomOrderDetailsService RoomOrderDetailsService @inject NavigationManager NavigationManager @inject HttpClient HttpClient // ... @code { // ... private async Task HandleCheckout() { if (!await HandleValidation()) { return; } try { HotelBooking.OrderDetails.ParbadTrackingNumber = -1; HotelBooking.OrderDetails.RoomId = HotelBooking.OrderDetails.HotelRoomDTO.Id; HotelBooking.OrderDetails.TotalCost = HotelBooking.OrderDetails.HotelRoomDTO.TotalAmount; var roomOrderDetailsSaved = await RoomOrderDetailsService.SaveRoomOrderDetailsAsync(HotelBooking.OrderDetails); await LocalStorage.SetItemAsync(ConstantKeys.LocalRoomOrderDetails, roomOrderDetailsSaved); var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder")) .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString()) .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString()) .Uri; NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } // ... }
namespace BlazorWasm.WebApi.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class RoomOrderController : Controller { private readonly IRoomOrderDetailsService _roomOrderService; public RoomOrderController(IRoomOrderDetailsService roomOrderService) { _roomOrderService = roomOrderService ?? throw new ArgumentNullException(nameof(roomOrderService)); } [HttpPost] public async Task<IActionResult> Create([FromBody] RoomOrderDetailsDTO details) { var result = await _roomOrderService.CreateAsync(details); return Ok(result); } [HttpGet] public async Task<IActionResult> GetOrderDetail(int trackingNumber) { var result = await _roomOrderService.GetOrderDetailByTrackingNumberAsync(trackingNumber); return Ok(result); } } }
- متد GetOrderDetail، بر اساس trackingNumber دریافتی از پرباد، کار بازگشت رکورد متناظری را انجام میدهد. از آن در پایان کار، جهت نمایش وضعیت پرداخت، استفاده میکنیم.
این دو متد در سرویس سمت سرور RoomOrderDetailsService، به صورت زیر تامین شدهاند:
namespace BlazorServer.Services { public class RoomOrderDetailsService : IRoomOrderDetailsService { // ... public async Task<RoomOrderDetailsDTO> CreateAsync(RoomOrderDetailsDTO details) { var roomOrder = _mapper.Map<RoomOrderDetail>(details); roomOrder.Status = BookingStatus.Pending; var result = await _dbContext.RoomOrderDetails.AddAsync(roomOrder); await _dbContext.SaveChangesAsync(); return _mapper.Map<RoomOrderDetailsDTO>(result.Entity); } public async Task<RoomOrderDetailsDTO> GetOrderDetailByTrackingNumberAsync(long trackingNumber) { var roomOrderDetailsDTO = await _dbContext.RoomOrderDetails .Include(u => u.HotelRoom) .ThenInclude(x => x.HotelRoomImages) .ProjectTo<RoomOrderDetailsDTO>(_mapperConfiguration) .FirstOrDefaultAsync(u => u.ParbadTrackingNumber == trackingNumber); roomOrderDetailsDTO.HotelRoomDTO.TotalDays = roomOrderDetailsDTO.CheckOutDate.Subtract(roomOrderDetailsDTO.CheckInDate).Days; return roomOrderDetailsDTO; } // ... } }
namespace BlazorWasm.Client.Services { public interface IClientRoomOrderDetailsService { Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details); Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber); } } namespace BlazorWasm.Client.Services { public class ClientRoomOrderDetailsService : IClientRoomOrderDetailsService { private readonly HttpClient _httpClient; public ClientRoomOrderDetailsService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<RoomOrderDetailsDTO> GetOrderDetailAsync(long trackingNumber) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, $"/api/roomorder/GetOrderDetail")) .AddParameter("trackingNumber", trackingNumber.ToString()) .Uri; return _httpClient.GetFromJsonAsync<RoomOrderDetailsDTO>(uri); } public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details) { details.UserId = "unknown user!"; var response = await _httpClient.PostAsJsonAsync("api/roomorder/create", details); var responseContent = await response.Content.ReadAsStringAsync(); if (response.IsSuccessStatusCode) { return JsonSerializer.Deserialize<RoomOrderDetailsDTO>(responseContent); } else { //var errorModel = JsonSerializer.Deserialize<ErrorModel>(responseContent); throw new InvalidOperationException(responseContent); } } } }
- متد SaveRoomOrderDetailsAsync، یک رکورد سفارش جدید را ایجاد میکند. در اینجا با روش مشاهدهی خطای کامل بازگشتی از سمت سرور (در صورت وجود) هم آشنا شدهایم که در مواقع لزوم میتواند راهگشا باشد.
- در متد SaveRoomOrderDetailsAsync فعلا مقدار UserId اجباری را به عبارتی دلخواه، تنظیم کردهایم. این مورد را در قسمتهای بعدی با معرفی اعتبارسنجی و احراز هویت سمت کلاینت، تکمیل خواهیم کرد.
این سرویس جدید را هم باید به سیستم تزریق وابستگیهای برنامهی کلاینت معرفی کرد تا قابل استفاده شود:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { // ... builder.Services.AddScoped<IClientRoomOrderDetailsService, ClientRoomOrderDetailsService>();
سپس به قطعه کد مهم زیر میرسیم:
var paymentUri = new UriBuilderExt(new Uri(HttpClient.BaseAddress, $"/api/ParbadPayment/PayRoomOrder")) .AddParameter("orderId", roomOrderDetailsSaved.Id.ToString()) .AddParameter("amount", roomOrderDetailsSaved.TotalCost.ToString()) .Uri; NavigationManager.NavigateTo(paymentUri.ToString(), forceLoad: true);
نمایش وضعیت پرداخت، به کاربر در پایان گردش کاری آن
پس از این مراحل، مرحلهی آخر کار باقی ماندهاست؛ یعنی بازگشت از اکشن متد VerifyRoomOrderPayment سمت سرور، به کامپوننت PaymentResult سمت کلاینت، برای نمایش نتیجهی عملیات. به همین جهت کامپوننت جدید Pages\HotelRooms\PaymentResult.razor را ایجاد کرده و به صورت زیر تکمیل میکنیم:
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}" @inject ILocalStorageService LocalStorage @inject IClientRoomOrderDetailsService RoomOrderDetailService @inject IJSRuntime JsRuntime @inject NavigationManager NavigationManager @if (IsLoading) { <div style="position:fixed;top:50%;left:50%;margin-top:-50px;margin-left:-100px;"> <img src="images/ajax-loader.gif" /> </div> } else { <div class="container"> <div class="row mt-4 pt-4"> <div class="col-10 offset-1 text-center"> @if(IsPaymentSuccessful) { <h2 class="text-success">Booking Confirmed!</h2> <p>Your room has been booked successfully with order id @OrderId & tracking number @TrackingNumber .</p> } else { <h2 class="text-warning">Booking Failed!</h2> <p>@Message</p> } <a class="btn btn-primary" href="hotel-rooms">Back to rooms</a> </div> </div> </div> } @code { private bool IsLoading; private bool IsPaymentSuccessful; [Parameter] public int OrderId { set; get; } [Parameter] public long TrackingNumber { set; get; } [Parameter] public string Message { set; get; } protected override async Task OnInitializedAsync() { IsLoading = true; try { var finalOrderDetail = await RoomOrderDetailService.GetOrderDetailAsync(TrackingNumber); var localOrderDetail = await LocalStorage.GetItemAsync<RoomOrderDetailsDTO>(ConstantKeys.LocalRoomOrderDetails); if(finalOrderDetail is not null && finalOrderDetail.IsPaymentSuccessful && finalOrderDetail.Status == BookingStatus.Booked && localOrderDetail is not null && localOrderDetail.TotalCost == finalOrderDetail.TotalCost) { IsPaymentSuccessful = true; await LocalStorage.RemoveItemAsync(ConstantKeys.LocalRoomOrderDetails); await LocalStorage.RemoveItemAsync(ConstantKeys.LocalInitialBooking); } else { IsPaymentSuccessful = false; } } catch(Exception ex) { await JsRuntime.ToastrError(ex.Message); } finally { IsLoading = false; } } }
@page "/payment-result/{OrderId:int}/{TrackingNumber:long}/{Message}"
جلوگیری از ثبت سفارش اتاقی که رزرو شدهاست
پس از پایان عملیات سفارش یک اتاق، بهتر است امکان سفارش اتاقی را که دیگر در دسترس نیست، غیرفعال کنیم (تصویر فوق) که اینکار را میتوان توسط خاصیت IsBooked مدل UI کامپوننت نمایش لیست اتاقها انجام داد:
public class HotelRoomDTO { public bool IsBooked { get; set; } // ... }
namespace BlazorServer.Services { public class HotelRoomService : IHotelRoomService { // ... public async Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDateStr, DateTime? checkOutDatestr) { var hotelRooms = await _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .Include(x => x.RoomOrderDetails) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .ToListAsync(); foreach (var hotelRoom in hotelRooms) { hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDateStr, checkOutDatestr); } return hotelRooms; } public async Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate) { var hotelRoom = await _dbContext.HotelRooms .Include(x => x.HotelRoomImages) .Include(x => x.RoomOrderDetails) .ProjectTo<HotelRoomDTO>(_mapperConfiguration) .FirstOrDefaultAsync(x => x.Id == roomId); hotelRoom.IsBooked = isRoomBooked(hotelRoom, checkInDate, checkOutDate); return hotelRoom; } private bool isRoomBooked(HotelRoomDTO hotelRoom, DateTime? checkInDate, DateTime? checkOutDate) { if (checkInDate == null || checkOutDate == null) { return false; } return hotelRoom.RoomOrderDetails.Any(x => x.IsPaymentSuccessful && //check if checkin date that user wants does not fall in between any dates for room that is booked ((checkInDate < x.CheckOutDate && checkInDate.Value.Date >= x.CheckInDate) //check if checkout date that user wants does not fall in between any dates for room that is booked || (checkOutDate.Value.Date > x.CheckInDate.Date && checkInDate.Value.Date <= x.CheckInDate.Date)) ); } } }
اکنون که خاصیت IsBooked مقدار دهی شدهاست، در دو قسمت از آن استفاده خواهیم کرد:
الف) در کامپوننت نمایش لیست اتاقها
@if (room.IsBooked) { <button disabled class="btn btn-secondary btn-block">Sold Out</button> } else { <a href="@($"hotel-room-details/{room.Id}")" class="btn btn-success btn-block">Book</a> }
@if (HotelBooking.OrderDetails.HotelRoomDTO.IsBooked) { <button disabled class="btn btn-secondary btn-block">Sold Out</button> } else { <button type="submit" class="btn btn-success form-control">Checkout Now</button> }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-30.zip