مقدمه
به عبارت دیگر در طراحی ساخت یافته، کلاسهای سطح بالا، به کلاسهای سطح پایین وابستهاند. این مسئله دو مشکل را ایجاد میکند:
یکی از اصول پنجگانهی طراحی برنامههای شیء گرا که با نام اصول SOLID شناخته میشوند، اصل «وارونگی وابستگیها» است که روشی برای مشکل جفت شدگی و وابستگی کلاسها به یکدیگر را به صورت تئوری ارائه میدهد.
در شکل زیر، حالت عادی جریان کنترل را میبینید .
شیء گرایی در واقع در مورد نحوهی جریان کنترل است. در اینجا اینترفیسها به ما کنترل کاملی را بر جریان کنترل (Flow of control) میدهند که با استفاده از این امکان میتوانیم از نوشتن کدهای جفت شده، شکننده و کلاسهایی یکبار مصرف، بپرهیزیم.
الگوی Dependency Injection
الگوی تزریق وابستگی 3 نوع کلاس را درگیر میکند:
شکل زیر وابستگی بین کلاسها را شرح میدهد:
همانطور که میبینید، کلاس Injector، نمونهای از کلاس سرویس را میسازد و آن را به نمونهای از کلاس Client تزریق میکند. با این کار، DI، وظیفهی ساخت یک نمونه از کلاس Service را از درون کلاس Client جدا میکند.
انواع تزریقات وابستگیها:
به صورت کلی به سه روش و در سه مکان، امکان تزریق وابستگی کلاس سرویس، درون کلاس کلاینت وجود دارد:
Inversion of Control Container
به صورت کلی IoC Container ها سه وظیفهی اساسی را برعهده دارند:
زمانیکه یک برنامه را بر پایهی شیء گرایی طراحی میکنید و مینویسید، به صورت معمول جریان وابستگیها در برنامهی شما به صورت زیر است:
در این حالت برای کامپایل شدن برنامه نیاز است که فرآیند کامپایل از دورترین کلاس و متد شروع شود. همانطور که میبینید در اینجا هر کلاس به تمام زیر کلاسهای خود وابسته است و هر تغییر در هر کدام از کلاسهای خدمتگزار میتواند تاثیرات مستقیمی بر روی سایر کلاسها داشته باشد. در واقع جریان کنترل برنامهی ما بجای اینکه در اختیار کلاسهای سیاست گذار ( کلاسهای بالایی در شکل) باشد، به دست کلاسهای خدمتگزار افتاده است. این قضیه باعث درهم تنیدگی و جفت شدگی کدها و کلاسها به یکدیگر میشود که مشکلزاست و امکان نگهداری، تغییرات و توسعهی برنامه را به شدت کاهش میدهد.
به عبارت دیگر در طراحی ساخت یافته، کلاسهای سطح بالا، به کلاسهای سطح پایین وابستهاند. این مسئله دو مشکل را ایجاد میکند:
- هر تغییری در کلاسهای سطح پایین ممکن است باعث ایجاد اشکالی در کلاسهای سطح بالا گردد.
- استفادهی مجدد از کلاسهای سطح بالا در جاهای دیگر مشکل است؛ زیرا وابستگی مستقیمی به کلاسهای سطح پایین دارند.
اصل معکوس سازی / وارونگی وابستگیها Dependency Inversion Principle
یکی از اصول پنجگانهی طراحی برنامههای شیء گرا که با نام اصول SOLID شناخته میشوند، اصل «وارونگی وابستگیها» است که روشی برای مشکل جفت شدگی و وابستگی کلاسها به یکدیگر را به صورت تئوری ارائه میدهد.
اصل وارونگی وابستگیها بیان میکند:
- ماژولهای (کلاسهای) سطح بالا نباید به ماژولهای (کلاسهای) سطح پایین وابسته باشند و هر دو باید به انتزاعات وابسته باشند (برای مثال interfaceها).
- انتزاعات نباید وابسته به جزئیات باشند؛ بلکه جزئیات (پیاده سازی) باید وابسته به انتزاعات باشند.
بر اساس این اصل، ما باید در سطوح بالا سیاست گذاریها و انتزاعات را در قالب interfaceها تعریف کرده و کلاسهای سطح بالای خود را بر همین اساس پیاده سازی کنیم و در سطوح پائینتر، پیاده سازیهایی را بر اساس انتزاعات و سیاست گذاریهای سطوح بالاتر، انجام دهیم.
همانطور که میبینید، کلاس M برای اجرا، وابسته به کلاس N و متد F در درون آن است. در اینجا ما با استفاده از اینترفیسها میتوانیم جریان کنترل را معکوس یا وارونه کنیم که به این عمل «وارونگی کنترل یا Inversion of Control» میگویند.
شیء گرایی در واقع در مورد نحوهی جریان کنترل است. در اینجا اینترفیسها به ما کنترل کاملی را بر جریان کنترل (Flow of control) میدهند که با استفاده از این امکان میتوانیم از نوشتن کدهای جفت شده، شکننده و کلاسهایی یکبار مصرف، بپرهیزیم.
الگوی Dependency Injection
تزریق وابستگی یا Dependency Injection، یک الگوی طراحی است که از آن برای طراحی و پیاده سازی IoC Containerها استفاده میشود. این الگو به ما اجازه میدهد که اشیاء وابسته را خارج از کلاس بسازیم و آنها را به طریقی دیگر به کلاس، جهت استفاده ارائه دهیم. بهوسیلهی DI ما ساخت و اتصال اشیاء وابسته به کلاس را از تعریف آن خارج میکنیم.
- کلاس کلاینت / Client Class : کلاس کلاینت (کلاس وابسته) کلاسی است که به کلاس سرویس وابسته است .
- کلاس سرویس / Service Class : کلاس سرویس (وابستگی) کلاسی است که یک سرویس را به کلاس کلاینت ارائه میدهد.
- کلاس تزریق کننده / Injector Class : کلاس تزریق کننده، نمونهای از کلاس سرویس را ساخته و به کلاس کلاینت، تزریق میکند.
شکل زیر وابستگی بین کلاسها را شرح میدهد:
همانطور که میبینید، کلاس Injector، نمونهای از کلاس سرویس را میسازد و آن را به نمونهای از کلاس Client تزریق میکند. با این کار، DI، وظیفهی ساخت یک نمونه از کلاس Service را از درون کلاس Client جدا میکند.
انواع تزریقات وابستگیها:
به صورت کلی به سه روش و در سه مکان، امکان تزریق وابستگی کلاس سرویس، درون کلاس کلاینت وجود دارد:
- تزریق درون سازنده / Constructor Injection : در تزریق درون سازنده، در سازندهی کلاس کلاینت، لیستی از سرویسهای مورد نیاز کلاس، که کلاس، برای عملکرد خود به آنها «وابسته» است، ثبت میشوند و کلاس Injector، سرویس (وابستگی) مورد نظر را درون سازندهی کلاس Client ارائه میدهد.
- تزریق درون Property کلاس / Property Injection : در این حالت که همچنین با نام (Setter Injection) هم شناخته میشود، تزریق کننده، وابستگی را به وسیلهی یک Property عمومی کلاس کلاینت ارائه میدهد.
- تزریق درون متد / Method Injection : در این حالت، خود کلاس کلاینت، یک پیاده سازی از یک interface را ارائه میکند که درون آن متدهایی برای ارائهی وابستگیها به کلاینت تعریف شدهاند. در این وضعیت، تزریق کننده از این اینترفیس برای ارائهی وابستگیها به کلاینت درون متدها، استفاده میکند.
هر کدام از روشهای فوق مزایا و معایب خود را دارند، ولی در NET Core. بیشتر از «تزریق درون سازنده» استفاده میشود. در صورت لزوم میتوانید از اینجا نمونههایی از تزریق وابستگی را به هر کدام از سه روش بالا، مشاهده کنید.
Inversion of Control Container
در واژگان فنی مهندسی نرم افزار، Container (محفظه) به جزیی از برنامه گفته میشود که میتواند اجزای دیگر برنامه را در بر بگیرد. IoC Container ها در واقع فریم ورکها/چارچوبها یا کتابخانههای برنامه نویسی هستند که ما در آنها میتوانیم اشیاء مختلف را به سبکهای خاصی تعریف و ثبت کنیم و در مواقع لزوم آنها را واکشی و به کلاسها تزریق کنیم. معمولا IoC Containerها لیستی از اشیاء هستند که در آن اینترفیسها و پیاده سازیهای مربوط به هر کدام از آنها ثبت میشوند. درون IoC Container برای پیاده سازی اصل وارونگی وابستگیها، معمولا از یکی از دو الگوی زیر استفاده میکنند (گاها هم دو الگو را با هم پیاده سازی میکنند) :
- Dependency Injection
- Service Locator
تمرکز اصلی ما در این نوشتار بر روی DI Container هاست. فرق Dependency Injection و Service Locator در این است که در DI، وابستگیها توسط IoC Container از درون محفظه واکشی میشوند و به درون کد تزریق میشوند ولی در Service Locator در هر جایی از برنامه میتوان با استفادهی مستقیم از Container و با استفاده از متدهایی که به ما ارائه میدهد، پرس و جو کرد (کوئری زد) و وابستگی مورد نظر را واکشی کرد.
در تزریق وابستگی، کلاس استفاده کننده از سرویسها، درگیر نحوهی واکشی و نمونه سازی از سرویس مورد نظر خود نمیشود و همهی کار توسط DI Container انجام میگیرد. ولی در Service Locator باید سرویس مورد نظر، درون خود کلاس، مستقیما از Container دریافت و ساخته شود.
برای استفاده از Service Locator، تنها پیش نیاز، دسترسی به شیء Service Locator است.
به صورت کلی IoC Container ها سه وظیفهی اساسی را برعهده دارند:
- ثبت سرویس درون خود
- ساخت نمونههای مورد نظر از سرویسها و ارائه دادن آنها به کلاسهایی که نیاز دارند.
- از بین بردن نمونه سرویسهای ساخته شده (Dispose) کردن آنها .
در ادامه با ساخت پروژهای، اولین سرویس خودمان را درون Microsoft Dependency Injection Container یا به اختصار DI Container، ثبت کرده و آن را واکشی میکنیم.
در این قسمت قصد داریم به بررسی Behavior ها در فریمورک MediatR بپردازیم. کدهای این قسمت بهروزرسانی و از این ریپازیتوری قابل دسترسی است.
در این صورت تمام متدهایی که نیاز به محاسبه زمان پردازش را دارند، باید به کلاسشان Logger تزریق شود. Stopwatch باید ایجاد، Start و Stop شود و در نهایت، بررسی کنیم که آیا زمان انجام این متد از حداکثری که برای آن مشخص کردهایم گذشته است یا خیر.
علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شدهاست، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شدهاست.
برای حل این مشکل از Behaviorها استفاده میکنیم. برای پیاده سازی Behaviorها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
همانطور که میبینید منطق کد ما تغییری نکردهاست. از IPipelineBehavior ارث بری کرده و متد Handle آن را پیاده سازی کردهایم. همانند Middleware ها در ASP.NET Core، در اینجا نیز یک RequestHandlerDelegate بنام next داریم که با اجرا و return آن، روند اجرای بقیه Command/Queryها ادامه پیدا خواهد کرد.
سپس باید Behaviorهای خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:
Transaction Behavior
یکی دیگر از استفادههای Behaviorها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Commandها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
سپس این Behavior را داخل DI Container خود Register میکنیم :
در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمتهای قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشدهاست.
MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.
با استفاده از Behaviorها امکان پیاده سازی AOP را براحتی خواهید داشت. Behaviorها، مانند Filter ها در ASP.NET MVC هستند. همانطور که با استفاده از متدهای OnActionExecuting و OnActionExecuted میتوانستیم اعمالی را قبل و بعد از اجرای یک اکشنمتد انجام دهیم، چنین قابلیتی را با Behaviorها در MediatR نیز خواهیم داشت. مزیت اینکار این است که شما میتوانید کدهای Cross-Cutting-Concern خود را یکبار نوشته و چندین بار بدون تکرار مجدد، از آن استفاده کنید.
فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را محاسبه کنیم، چنین کدی را تکرار کنیم:
Performance Counter Behavior
فرض کنید میخواهید زمان انجام کار یک متد را اندازه گیری کرده و در صورت طولانی بودن زمان انجام آن، لاگی را مبنی بر کند بودن بیش از حد مجاز این متد، ثبت کنید. شاید اولین راهی که برای انجام اینکار به ذهنتان بیاید این باشد که داخل تمام متدهایی که میخواهیم زمان انجام آنها را محاسبه کنیم، چنین کدی را تکرار کنیم:public class SomeClass { private readonly ILogger _logger; public SomeClass(ILogger logger) { _logger = logger; } public void SomeMethod() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); // TODO: Do some work here stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"SomeClass.SomeMethod has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } } }
علاوه بر این تصور کنید روزی تصمیم بگیرید که حداکثر زمان برای Log کردن را از 5 ثانیه به 10 ثانیه تغییر دهید. در این صورت بدلیل اینکه در همه متدها این قطعه کد تکرار شدهاست، مجبور به تغییر تمام کدهای برنامه برای اصلاح این بخش خواهید شد. در اینجا اصل DRY نقض شدهاست.
برای حل این مشکل از Behaviorها استفاده میکنیم. برای پیاده سازی Behaviorها داخل MediatR، کافیست از interface ای بنام IPipelineBehavior ارث بری کنیم:
public class RequestPerformanceBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<RequestPerformanceBehavior<TRequest, TResponse>> _logger; public RequestPerformanceBehavior(ILogger<RequestPerformanceBehavior<TRequest, TResponse>> logger) { _logger = logger; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); TResponse response = await next(); stopwatch.Stop(); if (stopwatch.ElapsedMilliseconds > TimeSpan.FromSeconds(5).Milliseconds) { // This method has taken a long time, So we log that to check it later. _logger.LogWarning($"{request} has taken {stopwatch.ElapsedMilliseconds} to run completely !"); } return response; } }
سپس باید Behaviorهای خود را از طریق DI به MediatR معرفی کنیم. داخل Startup.cs به این صورت RequestPerformanceBehavior خود را Register میکنیم:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceBehavior<,>));
در نهایت برای تست کارکرد این Behavior، در کوئری GetCustomerByIdQueryHandler خود 5 ثانیه Delay ایجاد میکنیم تا طول اجرای آن، از Maximum زمان مشخص شده بیشتر و Log انجام شود:
public class GetCustomerByIdQueryHandler : IRequestHandler<GetCustomerByIdQuery, CustomerDto> { private readonly ApplicationDbContext _context; private readonly IMapper _mapper; public GetCustomerByIdQueryHandler(ApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task<CustomerDto> Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken) { Customer customer = await _context.Customers .FindAsync(request.CustomerId); if (customer == null) { throw new RestException(HttpStatusCode.NotFound, "Customer with given ID is not found."); } // For testing PerformanceBehavior await Task.Delay(5000, cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
پس از اجرای برنامه و فراخوانی GetCustomerById ، داخل Console این پیغام را خواهید دید:
Transaction Behavior
یکی دیگر از استفادههای Behaviorها میتواند پیاده سازی Transaction و Rollback باشد. فرض کنید میخواهیم افزودن یک مشتری به دیتابیس فقط زمانی صورت گیرد که تمام کارهای داخل Command با موفقیت و بدون رخ دادن Exception انجام شود. برای انجام اینکار میتوان یک TransactionBehavior نوشت تا بدنه Commandها را داخل یک TransactionScope قرار دهد و در صورت وقوع Exception ، عمل Rollback صورت گیرد :
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { var transactionOptions = new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted, Timeout = TransactionManager.MaximumTimeout }; using (var transaction = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled)) { TResponse response = await next(); transaction.Complete(); return response; } } }
سپس این Behavior را داخل DI Container خود Register میکنیم :
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TransactionBehavior<,>));
در نهایت متد Handle در CreateCustomerCommandHandler را که در قسمتهای قبل ایجاد کردیم، تغییر داده و بعد از SaveChanges مربوط به Entity Framework، یک Exception را صادر میکنیم:
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; readonly IMediator _mediator; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper, IMediator mediator) { _context = context; _mapper = mapper; _mediator = mediator; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Domain.Customer customer = _mapper.Map<Domain.Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); throw new Exception("======= MY CUSTOM EXCEPTION ======="); // Raising Event ... await _mediator.Publish(new CustomerCreatedEvent(customer.FirstName, customer.LastName, customer.RegistrationDate), cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
اگر برنامه را اجرا کنید خواهید دید با اینکه Exception ما بعد از SaveChanges رخ داده است، اما بدلیل استفاده از Transaction Behavior ای که نوشتیم، عملیات Rollback صورت گرفته و داخل دیتابیس رکوردی ثبت نشدهاست.
MediatR دارای 2 اینترفیس IRequestPreProcessor و IRequestPostProcessor نیز هست که اگر نیاز داشته باشید یک عمل فقط قبل یا بعد از انجام یک Command/Query صورت گیرد، میتوانید از آنها استفاده کنید.
همچنین پیاده سازیهای پیشفرضی از این 2 اینترفیس با نامهای RequestPreProcessorBehavior و RequestPostProcessorBehavior داخل فریمورک، بطور پیشفرض وجود دارد که قبل و بعد از تمامی Handlerها اجرا خواهند شد.
درسته. ولی درحالت توسعه برنامه React اشاره میکند به آدرس https://localhost:3000/uploads/name.png (پورت 3000) و همین باعث میشود فایل ما(مانند تصویر) لود نشود. راه حل این مشکل چیست؟
در مطلب «فرمهای مبتنی بر قالبها در Angular - قسمت چهارم - اعتبارسنجی ورودیها» مشاهده کردیم که Angular در روش فرمهای مبتنی بر قالبها، تنها از 4 روش بومی اعتبارسنجی مرورگرها مانند ذکر ویژگی required برای فیلدهای اجباری، ویژگیهای minlength و maxlength برای تعیین حداقل و حداکثر تعداد حروف مجاز قابل ورود در یک فیلد و از pattern برای کار با عبارات با قاعده پشتیبانی میکند. برای بهبود این وضعیت در این مطلب قصد داریم روش تهیه اعتبارسنجهای سفارشی مخصوص حالت فرمهای مبتنی بر قالبها را بررسی کنیم.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
همچنین به فایل app.module.ts مراجعه کرده و CustomValidatorsModule را بجای CustomValidatorsRoutingModule در قسمت imports معرفی میکنیم. سپس به این ماژول جدید، کامپوننت فرم ثبت نام یک کاربر را اضافه خواهیم کرد:
که اینکار سبب به روز رسانی فایل custom-validators.module.ts و افزوده شدن UserRegisterComponent به قسمت declarations آن میشود.
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
با این محتوا:
در طراحی فرم HTML ایی آن نیاز است این موارد رعایت شوند:
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
این دستور علاوه بر ایجاد فایل جدید email-validator.directive.ts و تکمیل ساختار ابتدایی آن، کار به روز رسانی custom-validators.module.ts را نیز انجام میدهد. در این حالت به صورت خودکار قسمت declarations این ماژول با EmailValidatorDirective مقدار دهی میشود.
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
توضیحات تکمیلی:
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
- روش تعریف selector آن اندکی متفاوت است:
در اینجا مطابق https://angular.io/guide/styleguide#style-02-08 توصیه شدهاست که:
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
این مساله در جهت مشخص کردن سفارشی بودن این دایرکتیو و همچنین کاهش احتمال تکرار نامها توصیه شدهاست.
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
- پس از آن قسمت providers را مشاهده میکنید:
کار قسمت multi آن این است که EmailValidatorDirective (یا همان کلاس جاری) را به لیست NG_VALIDATORS توکار (اعتبارسنجهای توکار مبتنی بر قالبها) اضافه میکند و سبب بازنویسی هیچ موردی نخواهد شد. بنابراین وجود این قسمت در جهت تکمیل تامین کنندههای توکار Angular ضروری است.
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
این اینترفیس جزو مجموعهی فرمهای مبتنی بر قالبها است و از آن جهت نوشتن اعتبارسنجهای سفارشی میتوان استفاده کرد.
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
کار این متد این است که المانی را که appEmailValidator به آن اعمال شدهاست، به عنوان پارامتر متد validate در اختیار کلاس جاری قرار میدهد. به این ترتیب میتوان برای مثال به مقدار آن دسترسی یافت و سپس منطق سفارشی را پیاده سازی و یک خروجی key/value را بازگشت داد.
برای مثال در اینجا مقدار فیلد ایمیل element.value توسط عبارت باقاعدهی نوشته شده بررسی میشود. اگر با این الگو انطباق داشته باشد، نال بازگشت داده میشود (اعلام عدم وجود مشکلی در اعتبارسنجی) و اگر خیر، یک شیء key/value دلخواه را میتوان بازگشت داد.
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
و یا اگر اعتبارسنج استاندارد minlength باشکست مواجه شود، اطلاعات بیشتری را در قسمت مقدار این کلید بازگشتی، ارائه میدهد:
در کل اینکه چه چیزی را بازگشت دهید، بستگی به طراحی مدنظر شما دارد؛ برای نمونه در اینجا appEmailValidator (یک کلید و نام دلخواه است و هیچ الزامی ندارد که با نام selector این دایرکتیو یکی باشد)، به true تنظیم شدهاست:
بنابراین شرط تامین نوع خروجی، برقرار است. علت true بودن آن نیز مورد ذیل است:
در اینجا اگر false را بازگشت دهیم، هرچند email.errors دارای کلید جدید appEmailValidator شدهاست، اما ngIf سبب رندر خطای اعتبارسنجی «ایمیل وارد شده معتبر نیست.» به علت false بودن نتیجهی نهایی، نمیشود. یا حتی میتوان بجای true یک رشته و یا یک شیء با توضیحات بیشتری را نیز تنظیم کرد؛ چون value این key/value به any تنظیم شدهاست و هر چیزی را میپذیرد.
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
Angular به همراه اعتبارسنج توکاری برای بررسی یکی بودن این دو نیست. به همین جهت نمونهی سفارشی آنرا همانند EmailValidatorDirective فوق تهیه میکنیم. ابتدا یک دایرکتیو جدید را به نام EqualValidator به ماژول custom-validators اضافه میکنیم:
که سبب ایجاد فایل جدید equal-validator.directive.ts و به روز رسانی قسمت declarations فایل custom-validators.module.ts با EqualValidatorDirective نیز میشود.
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
توضیحات تکمیلی:
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
به این ترتیب زمانیکه قرار است فیلد کلمهی عبور را تعریف کنیم، ابتدا ویژگی appValidateEqual یا همان selector این اعتبارسنج به آن اضافه شدهاست تا کار فعال سازی ابتدایی صورت گیرد:
سپس Attribute یا ویژگی به نام compare-to نیز تعریف شدهاست. این compare-to همان نامی است که به Attribute@ نسبت داده شدهاست. سپس مقداری که به این ویژگی نسبت داده میشود، توسط خاصیت compareToControl دریافت خواهد شد.
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
در اینجا element.value مقدار المان یا کنترل HTML جاری است که appValidateEqual به آن اعمال شدهاست.
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
اگر کنترل دوم یافت شد و همچنین مقدار آن با مقدار کنترل جاری یکی نبود، همان شیء key/value مورد انتظار متد validate، در جهت اعلام شکست اعتبارسنجی بازگشت داده میشود.
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
اعتبارسنج تعریف شده، فقط به کنترلی که هم اکنون در حال کار با آن هستیم اعمال میشود. اگر پیشتر کلمهی عبوری را وارد کرده باشیم و سپس به فیلد تائید آن مراجعه کنیم، وضعیت اعتبارسنجی فیلد کلمهی عبور قبلی به حالت غیرمعتبر تنظیم شدهاست. اما پس از تکمیل فیلد تائید کلمهی عبور، هرچند وضعیت فیلد جاری معتبر است، اما هنوز وضعیت فیلد قبلی غیرمعتبر میباشد. برای رفع این مشکل، ابتدا کلید دلخواه appValidateEqual را از آن حذف میکنیم (همان کلیدی است که پیشتر در صورت مساوی نبودن مقدار فیلدها بازگشت داده شدهاست). حذف این کلید سبب نال شدن آرایهی errors یک شیء نمیشود و همانطور که پیشتر عنوان شد، Angular تنها به همین مورد توجه میکند. بنابراین در ادامه کار، setErrors یا تنظیم آرایهی errors به نال هم انجام شدهاست. در اینجا است که Angular فیلد دوم را نیز معتبر ارزیابی خواهد کرد.
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
در اینجا تنها کار مهمی که انجام شدهاست، ارائهی خاصیت عمومی مدل، جهت استفادهی از آن در قالب HTML ایی این کامپوننت است. بنابراین به فایل user-register.component.html مراجعه کرده و آنرا نیز به صورت ذیل تکمیل میکنیم:
ابتدای فرم
در اینجا novalidate اضافه شدهاست تا اعتبارسنجی توکار مرورگرها با اعتبارسنجی سفارشی فرم جاری تداخل پیدا نکند. همچنین توسط یک template reference variable به وهلهای از فرم دسترسی یافته و آنرا به متد submitForm کامپوننت ارسال کردهایم.
تکمیل قسمت ورود نام کاربری
اعتبارسنجی فیلد نام کاربری شامل سه قسمت بررسی errors.required، errors.minlength و errors.maxlength است.
تکمیل قسمت ورود ایمیل
در اینجا نحوهی استفادهی از دایرکتیو جدید appEmailValidator را ملاحظه میکنید. این دایرکتیو ابتدا به المان فوق متصل و سپس نتیجهی آن در قسمت ngIf، برای نمایش خطای متناظری بررسی شدهاست.
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
در اینجا نحوهی اعمال دایرکتیو جدید appValidateEqual و همچنین ویژگی compare-to آنرا به فیلدهای کلمهی عبور و تائید آن مشاهده میکنید.
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
در اینجا بررسی میشود که آیا فرم معتبر است یا خیر. اگر خیر، دکمهی submit آن غیرفعال میشود و برعکس.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
و در دومی دستورات ذیل را اجرا کنید:
اکنون میتوانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
تدارک مقدمات مثال این قسمت
این مثال، در ادامهی همین سری کار با فرمهای مبتنی بر قالبها است. به همین جهت ابتدا ماژول جدید CustomValidators را به آن اضافه میکنیم:
>ng g m CustomValidators -m app.module --routing
>ng g c CustomValidators/user-register
در ادامه کلاس مدل معادل فرم ثبت نام کاربران را تعریف میکنیم:
>ng g cl CustomValidators/user
export class User { constructor( public username: string = "", public email: string = "", public password: string = "", public confirmPassword: string = "" ) {} }
- ورود نام کاربری اجباری بوده و باید بین 5 تا 8 حرف باشد.
- ورود ایمیل اجباری بوده و باید فرمت مناسبی نیز داشته باشد.
- ورود کلمهی عبور اجباری بوده و باید با confirmPassword تطابق داشته باشد.
- ورود «کلمهی عبور خود را مجددا وارد کنید» اجباری بوده و باید با password تطابق داشته باشد.
تعریف اعتبارسنج سفارشی ایمیلها
هرچند میتوان اعتبارسنجی ایمیلها را توسط ویژگی استاندارد pattern نیز مدیریت کرد، اما جهت بررسی نحوهی انتقال آن به یک اعتبارسنج سفارشی، کار را با ایجاد یک دایرکتیو مخصوص آن ادامه میدهیم:
>ng g d CustomValidators/EmailValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج سفارشی را مشاهده میکنید:
import { Directive } from "@angular/core"; import { AbstractControl, NG_VALIDATORS, Validator } from "@angular/forms"; @Directive({ selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true } ] }) export class EmailValidatorDirective implements Validator { validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; } }
- علت تعریف این اعتبارسنج به صورت یک دایرکتیو جدید این است که بتوان selector آنرا همانند ویژگیهای HTML، به فیلد ورودی اضافه کرد:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
- روش تعریف selector آن اندکی متفاوت است:
selector: "[appEmailValidator][formControlName],[appEmailValidator][formControl],[appEmailValidator][ngModel]",
الف) نام دایرکتیو باید با یک پیشوند شروع شود و این پیشوند در فایل angular-cli.json. به app تنظیم شدهاست:
"apps": [ { // ... "prefix": "app",
ب) در اینجا formControlName، formControl و ngModel قید شدهی در کنار نام selector این دایرکتیو را نیز مشاهده میکنید. وجود آنها به این معنا است که کلاس این دایرکتیو، به المانهایی که به آنها ویژگی appEmailValidator اضافه شدهاست و همچنین آن المانها از یکی از سه نوع ذکر شده هستند، اعمال میشود و در سایر موارد بیاثر خواهد بود. البته ذکر این سه نوع، اختیاری است و صرفا میتوان نوشت:
selector: "[appEmailValidator]"
- پس از آن قسمت providers را مشاهده میکنید:
providers: [ { provide: NG_VALIDATORS, useExisting: EmailValidatorDirective, multi: true }
- سپس پیاده سازی اینترفیس توکار Validator را مشاهده میکنید:
export class EmailValidatorDirective implements Validator {
برای پیاده سازی این اینترفیس، نیاز است متد اجباری ذیل را نیز افزود و تکمیل کرد:
validate(element: AbstractControl): { [key: string]: any }
validate(element: AbstractControl): { [key: string]: any } { const emailRegex = /\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/; const valid = emailRegex.test(element.value); return valid ? null : { appEmailValidator: true }; }
- اکنون که این دایرکتیو جدید طراحی و ثبت شدهاست (در قسمت declarations فایل custom-validators.module.ts)، تنها کافی است selector آنرا به المان ورودی مدنظر اعمال کنیم تا کار اعتبارسنجی آنرا به صورت خودکار مدیریت کند:
<input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email">
نحوهی طراحی خروجی متد validate
هنگام پیاده سازی متد validate اینترفیس Validator، هیچ قالب خاصی برای خروجی آن درنظر گرفته نشدهاست و همینقدر که این خروجی یک شیء key/value باشد، کفایت میکند. برای مثال اگر اعتبارسنج استاندارد required با شکست مواجه شود، یک چنین شیءایی را بازگشت میدهد:
{ required:true }
{ minlength : { requiredLength : 3, actualLength : 1 } }
{ appEmailValidator: true }
<div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div>
از دیدگاه اعتبارسنج فرمهای مبتنی بر قالبها، همینقدر که آرایهی email.errors دارای عضو و کلید جدیدی شد، کار به پایان رسیدهاست و اعتبارسنجی المان را شکست خورده ارزیابی میکند. مابقی آن، اطلاعاتی است که برنامه نویس ارائه میدهد (بر اساس نیازهای نمایشی برنامه).
تهیه اعتبارسنج سفارشی مقایسهی کلمات عبور با یکدیگر
در طراحی کلاس User که معادل فیلدهای فرم ثبت نام کاربران است، دو خاصیت کلمهی عبور و تائید کلمهی عبور را مشاهده میکنید:
public password: string = "", public confirmPassword: string = ""
>ng g d CustomValidators/EqualValidator -m custom-validators.module
در ادامه کدهای کامل آنرا در ذیل مشاهده میکنید:
import { Directive, Attribute } from "@angular/core"; import { Validator, AbstractControl, NG_VALIDATORS } from "@angular/forms"; @Directive({ selector: "[appValidateEqual][formControlName],[appValidateEqual][formControl],[appValidateEqual][ngModel]", providers: [ { provide: NG_VALIDATORS, useExisting: EqualValidatorDirective, multi: true } ] }) export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null }); if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; } if ( otherControl && otherControl.errors && selfValue === otherControl.value ) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null; } }
- قسمت آغازین این اعتبارسنج سفارشی، مانند توضیحات EmailValidatorDirective است که در ابتدای بحث عنوان شد. این کلاس به یک Directive مزین شدهاست تا بتوان selector آنرا به المانهای HTML ایی فرم افزود (برای مثال در اینجا به دو فیلد ورود کلمات عبور). قسمت providers آن نیز تنظیم شدهاست تا EqualValidatorDirective جاری به لیست توکار NG_VALIDATORS اضافه شود.
- در ابتدای کار، پیاده سازی اینترفیس Validator، همانند قبل انجام شدهاست؛ اما چون در اینجا میخواهیم نام فیلدی را که قرار است کار مقایسه را با آن انجام دهیم نیز دریافت کنیم، ابتدا یک Attribute و سپس یک پارامتر و خاصیت عمومی دریافت کنندهی مقدار آنرا نیز افزودهایم:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {}
<input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password">
در اینجا محدودیتی هم از لحاظ تعداد ویژگیها نیست و اگر قرار است این اعتبارسنج اطلاعات بیشتری را نیز دریافت کند میتوان ویژگیهای بیشتری را به سازندهی آن نسبت داد.
یک نکته: میتوان نام این ویژگی را با نام selector نیز یکی انتخاب کرد. به این ترتیب ذکر نام ویژگی آن، هم سبب فعال شدن اعتبارسنج و هم نسبت دادن مقداری به آن، سبب مقدار دهی خاصیت متناظر با آن، در سمت کلاس اعتبارسنج میگردد.
- در ابتدای این اعتبارسنج، نحوهی دسترسی به مقدار یک کنترل دیگر را نیز مشاهده میکنید:
export class EqualValidatorDirective implements Validator { constructor(@Attribute("compare-to") public compareToControl: string) {} validate(element: AbstractControl): { [key: string]: any } { const selfValue = element.value; const otherControl = element.root.get(this.compareToControl); console.log("EqualValidatorDirective", { thisControlValue: selfValue, otherControlValue: otherControl ? otherControl.value : null });
بر اساس مقدار خاصیت compareToControl که از ویژگی compare-to دریافت میشود، میتوان به کنترل دوم، توسط element.root.get دسترسی یافت.
- در ادامهی کار، مقایسهی سادهای را مشاهده میکنید:
if (otherControl && selfValue !== otherControl.value) { return { appValidateEqual: true // Or a string such as 'Password mismatch.' or an abject. }; }
- در پایان کدهای متد validate، چنین تنظیمی نیز قرار گرفتهاست:
if (otherControl && otherControl.errors && selfValue === otherControl.value) { delete otherControl.errors["appValidateEqual"]; if (!Object.keys(otherControl.errors).length) { otherControl.setErrors(null); } } return null;
تکمیل کامپوننت فرم ثبت نام کاربران
اکنون user-register.component.ts را که در ابتدای بحث اضافه کردیم، چنین تعاریفی را پیدا میکند:
import { NgForm } from "@angular/forms"; import { User } from "./../user"; import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-user-register", templateUrl: "./user-register.component.html", styleUrls: ["./user-register.component.css"] }) export class UserRegisterComponent implements OnInit { model = new User(); constructor() {} ngOnInit() {} submitForm(form: NgForm) { console.log(this.model); console.log(form.value); } }
ابتدای فرم
<div class="container"> <h3>Registration Form</h3> <form #form="ngForm" (submit)="submitForm(form)" novalidate>
تکمیل قسمت ورود نام کاربری
<div class="form-group" [class.has-error]="username.invalid && username.touched"> <label class="control-label">User Name</label> <input #username="ngModel" required maxlength="8" minlength="4" type="text" class="form-control" name="username" [(ngModel)]="model.username"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-info"> errors: {{ username.errors | json }} </div> <div class="alert alert-danger" *ngIf="username.errors.required"> username is required. </div> <div class="alert alert-danger" *ngIf="username.errors.minlength"> username should be minimum {{username.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="username.errors.maxlength"> username should be max {{username.errors.maxlength.requiredLength}} characters. </div> </div> </div>
تکمیل قسمت ورود ایمیل
<div class="form-group" [class.has-error]="email.invalid && email.touched"> <label class="control-label">Email</label> <input #email="ngModel" required appEmailValidator type="text" class="form-control" name="email" [(ngModel)]="model.email"> <div *ngIf="email.invalid && email.touched"> <div class="alert alert-info"> errors: {{ email.errors | json }} </div> <div class="alert alert-danger" *ngIf="email.errors.required"> email is required. </div> <div class="alert alert-danger" *ngIf="email.errors.appEmailValidator"> The entered email is not valid. </div> </div> </div>
تکمیل قسمتهای ورود کلمهی عبور و تائید آن
<div class="form-group" [class.has-error]="password.invalid && password.touched"> <label class="control-label">Password</label> <input #password="ngModel" required type="password" class="form-control" appValidateEqual compare-to="confirmPassword" name="password" [(ngModel)]="model.password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-info"> errors: {{ password.errors | json }} </div> <div class="alert alert-danger" *ngIf="password.errors.required"> password is required. </div> <div class="alert alert-danger" *ngIf="password.errors.appValidateEqual"> Password mismatch. Please complete the confirmPassword . </div> </div> </div> <div class="form-group" [class.has-error]="confirmPassword.invalid && confirmPassword.touched"> <label class="control-label">Retype password</label> <input #confirmPassword="ngModel" required type="password" class="form-control" appValidateEqual compare-to="password" name="confirmPassword" [(ngModel)]="model.confirmPassword"> <div *ngIf="confirmPassword.invalid && confirmPassword.touched"> <div class="alert alert-info"> errors: {{ confirmPassword.errors | json }} </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.required"> confirmPassword is required. </div> <div class="alert alert-danger" *ngIf="confirmPassword.errors.appValidateEqual"> Password mismatch. </div> </div> </div>
همچنین خروجی آن نیز در قسمت ngIf آخر بررسی شدهاست و سبب نمایش خطای اعتبارسنجی متناسبی میشود.
تکمیل انتهای فرم
<button class="btn btn-primary" [disabled]="form.invalid" type="submit">Ok</button> </form> </div>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
نظرات اشتراکها
درس خوندن، ارزشش رو داره؟
گذشته از بحث مدرک محوری که متاسفانه کشور رو به جهت نامناسبی کشونده و باعث خیلی بی عدالتیها و عقب ماندگیها و ... در کشور شده (به خصوص در سیستم هایی که مستقیم یا غیر مستقیم دولتی هستند!)، ماهیت دانشگاه بسیار مهم است.
در دانشگاه چیزهایی مثل نحوه یادگیری، کارگروهی، بالا بردن انگیزه، حس رقابت، حس پیشرفت، اعتماد به نفس و خیلی چیزهای دیگه به طور عادی یا اجباری یاد داده میشود که کسی که دانشگاه نرفته قطعا این موارد رو هم تجربه نخواهد کرد بنابراین دیدگاه یک شخص تحصیل کرده از زمین تا آسمان با نمونه دانشگاه نرفته آن تفاوت دارد. (صد البته رتبه دانشگاه نیز در یادگیری و میزان این تجربیات تاثیر فراوان دارد یعنی نمیشه دو تا فارغ التحصیل دانشگاه رو که مثلا یکی در صنعتی اصفهان درس خونده رو با دانشگاه غیر انتفاعی فلان شهرستان(!) مقایسه کرد که از لحاظ وسعت اندازه یک مدرسه هم نیست و هر دو هم کارشناس تربیت میکنند.)
اما برای بچههای نرمافزار به نظرم قضیه حتی مهمتر از مابقی رشتهها هم هست. یعنی عقیده دارم دانشگاه رفتن برای کسایی که دوست دارند تو حوزه نرمافزار کارآفرین، کارشناس، یا هرچیزه دیگه ای بشن خیلی مهمه. چرا؟
(برای مثال) تو دانشگاه اسمبلی به شما یاد میدهند به همراه ریزپردازنده و معماری کامپیوتر که با ترکیب نظریه زبانها و کامپایلر دانشجو میفهمه از زمانی که یک خط در کامپیوتر به عنوان برنامه در هر زبانی نوشته میشه این یک خط چطور توسط کامپایلر از لحاظ نحو، دستور بررسی میشه و چطور کد معادل سطح پایین ایجاد میشه و کد رو پردازنده چطوری با کمک چه ثباتهایی و چه دستوراتی در سطح ماشین اجرا میکنه.
یا درسی مثل طراحی الگوریتمهاست که انواع و اقسام الگوریتمهای مختلف که تا الان ارائه شدند بررسی میکنه و از لحاظ سرعت و زمان (Order) مورد تحلیل قرار میده که مثلا الگوریتم Quick Sort در چه زمانی یک لیست رو مرتب میکنه و Bubble Sort در چه زمانی و یا اینکه ضرب یک ماتریس n*n در حالت تک پردازنده و یا بصورت موازی چطور انجام میشه و چقدر زمان نیاز داره؟
یا شبیه سازی پنجره ویندوز در مد گرافیک در زبان C چه کتابخانهها و سختافزارهایی رو درگیر میکنه و یا منظور از نرمال سازی در سطح 3NF در مفاهیم پایگاه داده رابطهای چی هست و چطور میشه به اون رو در واقعیت رسید. (که خیلی از مدعیان کار با MS SQL SERVER یا ... هستند که حتی اصول و مفاهیم ساده پایگاه داده رو هم نمیدونند.)
اینها چیزهایی هستند که کسانی که دانشگاه نرفتن و صرفا با یادگیری یک زبان برنامه نویسی وارد این حوزه شدند و حتی مدعی هم هستند(!)، نمیدانند.
ممکنه فکر کنید خوب دونستن اینها چه فایدهای داره؟
زمانی که سرباز بودم از مافوقم پرسیدم با وجود اینهمه سلاح اتوماتیک و سبک و کوچک و جدید چرا ما باید از کلاشینکف صد سال پیش روسی استفاده کنیم؟ جواب این بود که با یادگیری کامل این سلاح میتونید تقریبا با همه سلاحهای موجود کار کنید و کلیات ماجرای همه این سلاحها از همین کلاشینکف و ژ-۳ ارث بریمیکند(!) و یادگیری بقیه با دونستن اطلاعات پایه تنها یادگیری بخش جدیدی که به این سلاحها اضافه شده وگرنه پایه همان است.
شبیه همین جواب را زمانی شنیدم که از استادم پرسیدم یادگیری ساختار پردازنده 80X86 زمانی که الان پردازنده با ساختار چند هستهای توسط اینتل تولید میشه چه فایدهای داره؟
بنابراین کسی که واقعا این مسائل پایهای رو خوب یاد گرفته باشه توانایی فوقالعادهای در درک مسائل جدید در آینده خواهد داشت و قطعا محصول بسیار باکیفیتتر و بهینهتری تولید خواهد کرد و ذهن بسیار خلاقتری خواهد داشت و اگر شرایط اجتماعی برایش فراهم باشد موجب افتخار یک کشور نیز خواهد شد.
نکته آخر:
اگر فیلم The Social Network رو دیده باشید حتما اون بخشی رو که مارک زاکربرگ وارد کلاس دانشگاه هاروارد میشه و با بیحوصلگی تمام سرکلاس به مطالب استاد گوش میده و میخواد کلاس رو ترک کنه که استاد ازش یک سوال درسی میپرسه (برای مسخره کردنش!) و مارک ایستاده و روی پلهها جواب استاد رو میده که هیج ، کمی جلوتر از فکر استاد پیش میره و جواب میده که کلاس هنوز به اون بخش نرسیده و همه مات و مبهوت موندهاند...
بله، درسته خیلی از بزرگان این رشته دانشگاه نرفتهاند و یا ترک تحصیل کردهاند ولی علت این بوده که درسهای ارائه شده در دانشگاه اونها رو سیراب نمیکرده و اونها نیاز به چیزی فرای درسهای ساده دانشگاه داشتند که در دانشگاه هیچ وقت به اون نمیرسیدند.
این افراد رو نمیشه با کسانی که در دانشگاه برای پاس کردن فلان درس به هر دری میزنند تا به مدرک برسن یا ذهنشون توانایی حل یک معادله ساده درجه ۲ رو نداره که بخوان براش برنامه بنویسن، یکی کرد.
بنابراین امثال جابز و گیتس نوابغ بشری بودند و هستند که تونستند در جامعهای که زمینه براشون مهیا بوده بدون نیاز به دانشگاه موفق بشن و شهرت جهانی پیدا کنند.
موفق باشید.
مطالب
آموزش Knockout.Js #3
در ادامه مباحث قبلی، در این پست به بررسی سایر قابلیتهای Observableها در KO خواهم پرداخت.
Computed Observables
Computed Observablesها به واقع خواصی هستندکه از ترکیب چند خاصیت دیگر به دست میآیند یا برای به دست آوردن مقادیر آنها باید یک سری محاسبات را انجام داد. برای مثال به ViewModel زیر دقت کنید:
همان طور که مشخص است یک خاصیت به نام fullName ایجاد کردم که از ترکیب خواص firstName و lastName به دست آمده است. برای ایجاد این گونه خواص باید از دستور ko.compute استفاده شود که پارامتر ورودی آن یک تابع برای برگشت مقدار مورد نظر است. برای مقید کردن این خاصیت به کنترل مورد نظر نیز همانند قبل عمل خواهیم نمود:
آرایه ای از Observable
برای ایجاد یک Observable Array باید از دستور ko.observableArray استفاده کنیم که ورودی آن نیز مجموعه ای از داده مورد نظر است:
در ابتدا یک لیست با سه مقدار خواهیم داشت. برای نمایش لیست، نیاز به یک جدول داریم که کد آن به صورت زیر خواهد بود:
یک توضیح : همانطور که میبینید در تگ <tbody> از دستور foreach برای پیمایش لیست مورد نظر(shoppingCart) استفاده شده است. برای مقید سازی تگهای <td> به مقادیر ViewModel از data-bind attribute استفاده شده است.
حال نیاز به یک button داریم تا با کلیک با بر روی آن یک product جدید به لیست اضافه خواهد شد.
در ViewModel یک تابع جدید به نام addProduct ایجاد میکنیم :
از دستور push برای اضافه کردن یک آیتم به لیست اضافه میشود.
تا اینجا کدهای ViewModel به صورت زیر خواهد بود:
دریافت سورس مثال تا اینجا
در این مرحله قصد داریم که یک button نیز برای حذف آیتم از لیست ایجاد کنیم. در ابتدا یک تایع جدید به نام removeProduct به صورت زیر ایجاد خواهیم کرد:
با کمی دقت متوجه خواهید شد که به جای this از self استفاده شده است. در واقع self چیزی نیست جز یک اشاره گر به viewModel جاری. اگر از this استفاده کنید با یک TypeError مواجه خواهید شد و برای جلوگیری از این خطا باید در ابتدای ViewModel دستور زیر را بنویسیم:
و در کدهای Html جدول مورد نظر نیز باید تغییرات زیر را اعمال کنیم:
به ازای هر محصول یک button داریم که البته رویداد کلیک آن به تابع removeProduct عنصر جاری مقید شده است(root$ به عنصر جاری در لیست اشاره میکند).
دستور remove در لیست باعث حذف کامل آیتم از لیست خواهد شد و در خیلی موارد این مورد برای ما خوشایند نیست زیرا حذف یک آیتم از لیست باید در سمت سرور نیز انجام شود نه صرفا در سمت کلاینت، در نتیجه میتوانیم از دستور destroy استفاده کنیم. استفاده از این دستور باعث خواهد شد که عنصر مورد نظر در لیست نمایش داده نشود ولی به صورت واقعی از لیست حذف نشده است(این کار را با تغییر در مقدار خاصیت destroy_ هر عنصر انجام میدهد)
ادامه دارد...
دریافت سورس مثال
Computed Observables
Computed Observablesها به واقع خواصی هستندکه از ترکیب چند خاصیت دیگر به دست میآیند یا برای به دست آوردن مقادیر آنها باید یک سری محاسبات را انجام داد. برای مثال به ViewModel زیر دقت کنید:
var personViewModel = { firstName: ko.observable("Masoud"), lastName: ko.observable("Pakdel") this.fullName = ko.computed(function() { return this.firstName() + " " + this.lastName(); }, this); };
<span data-bind='text: fullName'></span>
برای ردیابی و مشاهده تغییرات در یک آرایه باید از Observable array استفاده نماییم. برای درک بهتر موضوع یک مثال را پیاده سازی خواهیم کرد: در این مثال یک لیست از محصولات مورد نظر را داریم به همراه یک button برای اضافه کردن محصول جدید. بعد از کلیک بر روی دکمه مورد نظر، بک محصول جدید، به لیست اضافه خواهد شد و تغییرات لیست در لحظه مشاهده خواهد شد.
ابتدا باید مدل مورد نظر را ایجاد کنیم. function Product(name, price) { this.name = ko.observable(name); this.price = ko.observable(price); }
this.shoppingCart = ko.observableArray([ new Product("Beer", 10.99), new Product("Brats", 7.99), new Product("Buns", 1.49) ]);
<table> <thead><tr> <th>Product</th> <th>Price</th> </tr></thead> <tbody data-bind='foreach: shoppingCart'> <tr> <td data-bind='text: name'></td> <td data-bind='text: price'></td> </tr> </tbody> </table>
حال نیاز به یک button داریم تا با کلیک با بر روی آن یک product جدید به لیست اضافه خواهد شد.
<button data-bind='click: addProduct'>Add Beer</button>
this.addProduct = function() { this.shoppingCart.push(new Product("More Beer", 10.99)); };
تا اینجا کدهای ViewModel به صورت زیر خواهد بود:
function PersonViewModel() { this.firstName = ko.observable("John"); this.lastName = ko.observable("Smith"); this.checkout = function () { alert("Trying to checkout"); }; this.fullName = ko.computed(function(){ return this.firstName() + " " + this.lastName(); }, this); this.shoppingCart = ko.observableArray([ new Product("Beer", 10.99), new Product("Brats", 7.99), new Product("Buns", 1.49) ]); this.addProduct = function () { this.shoppingCart.push(new Product("More beer", 10.99)); }; };
در این مرحله قصد داریم که یک button نیز برای حذف آیتم از لیست ایجاد کنیم. در ابتدا یک تایع جدید به نام removeProduct به صورت زیر ایجاد خواهیم کرد:
this.removeProduct = function(product) { self.shoppingCart.remove(product); };
function PersonViewModel() { var self = this;
<tr> <td data-bind='text: name'></td> <td data-bind='text: price'></td> <td><button data-bind='click: $root.removeProduct'>Remove</button></td> </tr>
دستور remove در لیست باعث حذف کامل آیتم از لیست خواهد شد و در خیلی موارد این مورد برای ما خوشایند نیست زیرا حذف یک آیتم از لیست باید در سمت سرور نیز انجام شود نه صرفا در سمت کلاینت، در نتیجه میتوانیم از دستور destroy استفاده کنیم. استفاده از این دستور باعث خواهد شد که عنصر مورد نظر در لیست نمایش داده نشود ولی به صورت واقعی از لیست حذف نشده است(این کار را با تغییر در مقدار خاصیت destroy_ هر عنصر انجام میدهد)
ادامه دارد...
دریافت سورس مثال
نظرات مطالب
تقسیم جدول در Entity Framework Code First
با تشکر از مطلب خوبتون ..
به نظرتون اینکه در کوئری نهایی ایجاد شده ، EF از 2 دستور Select تو در تو استفاده کرده باعث کاهش سرعت اجرای کوئری نمیشه ؟
ببخشید؛ من بخش آخر را متوجه نشدم. هنگامی که تغییری در یک جدول ایجاد میکنیم با دستور this .InvalidateSecondLevelCache(); کل کش را غیره معتبر میکند یا فقط جدولی که تغییرات داشته است ؟
نظرات مطالب
فیلدهای پویا در NHibernate
این طور که به نظر میرسد برای مثال در صورت اضافه نمودن یک فیلد جدید به Attributes در این مثال، باید بانک اطلاعات مجدداً ایجاد شود. آیا این طور است؟
به علت خروجی دستور Insert و Select به این نتیجه رسیدم.
به علت خروجی دستور Insert و Select به این نتیجه رسیدم.