روش استفاده از نوع DateOnly در دات نت 6
نوعهای جدید معرفی شده، بسیار واضح هستند و مقصود از بکارگیری آنها را به خوبی بیان میکنند. برای مثال اگر نیاز بود تاریخی را بدون در نظر گرفتن قسمت زمان آن معرفی کنیم، میتوان از نوع DateOnly استفاده کرد؛ مانند تاریخ تولد، روزهای کاری و امثال آن. تا پیش از این برای معرفی یک چنین تاریخهایی، عموما قسمت زمان DateTime را با 00:00:00.000 مقدار دهی میکردیم؛ اما دیگر نیازی به این نوع تعاریف نیست و میتوان مقصود خود را صریحتر بیان کرد.
روش معرفی نمونهای از آن با معرفی سال، ماه و روز است:
var date = new DateOnly(2020, 04, 20);
var currentDate = DateOnly.FromDateTime(DateTime.Now);
همچنین در اینجا نیز همانند DateTime میتوان از متدهای Parse و یا TryParse، برای تبدیل یک رشته به معادل DateOnly آن، کمک گرفت:
if (DateOnly.TryParse("28/09/1984", new CultureInfo("en-US"), DateTimeStyles.None, out var result)) { Console.WriteLine(result); }
و یا میتوان توسط متد ParseExact، ساختار تاریخ دریافتی را دقیقا مشخص کرد:
DateOnly d1 = DateOnly.ParseExact("31 Dec 1980", "dd MMM yyyy", CultureInfo.InvariantCulture); // Custom format Console.WriteLine(d1.ToString("o", CultureInfo.InvariantCulture)); // "1980-12-31" (ISO 8601 format)
در حین نمونه سازی DateOnly، امکان ذکر تقویمهای خاص، مانند PersianCalendar نیز وجود دارد:
var persianCalendar = new PersianCalendar(); DateOnly d2 = new DateOnly(1400, 9, 6, persianCalendar); Console.WriteLine(d2.ToString("d MMMM yyyy", CultureInfo.InvariantCulture));
در اینجا همچنین متدهایی مانند AddDays، AddMonths و AddYears نیز بر روی date مهیا کار میکنند:
var newDate = date.AddDays(1).AddMonths(1).AddYears(1)
یک نکته: برخلاف DateTime، نوع DateOnly به همراه DateTimeKind مانند Utc و امثال آن نیست و همواره DateTimeKind آن Unspecified است.
روش استفاده از نوع TimeOnly در دات نت 6
نوع و ساختار TimeOnly، قسمت زمان را به نحو صریحی مشخص میکند؛ مانند ساعتی که باید هر روز راس آن، آلارمی به صدا درآید و یا جلسهای تشکیل شود و یا وظیفهای صورت گیرد. سازندهی آن overloadهای قابل توجهی را داشته و میتواند یکی از موارد زیر باشد:
public TimeOnly(int hour, int minute) public TimeOnly(int hour, int minute, int second) public TimeOnly(int hour, int minute, int second, int millisecond)
var startTime = new TimeOnly(10, 30);
var endTime = new TimeOnly(13, 00, 00);
و یا برای مثال میتوان این نمونهها را از هم کم کرد:
var diff = endTime - startTime;
Console.WriteLine($"Hours: {diff.TotalHours}");
TimeSpan ts = endTime.ToTimeSpan();
برای تبدیل قسمت زمان DateTime به TimeOnly، میتوان از متد FromDateTime به صورت زیر استفاده کرد:
var currentTime = TimeOnly.FromDateTime(DateTime.Now);
DateTime dt = date.ToDateTime(new TimeOnly(0, 0)); Console.WriteLine(dt);
و در این حالت اگر خواستیم بررسی کنیم که آیا زمانی بین دو زمان دیگر واقع شدهاست یا خیر، میتوان از متد IsBetween استفاده نمود:
var isBetween = currentTime.IsBetween(startTime, endTime); Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end");
در اینجا امکان مقایسه این نمونهها، توسط عملگرهایی مانند < نیز وجود دارد:
var startTime = new TimeOnly(08, 00); var endTime = new TimeOnly(09, 00); Console.WriteLine($"{startTime < endTime}");
اگر نیاز به تبدیل رشتهای به TimeOnly بود، میتوان از متد ParseExact به همراه ذکر ساختار مدنظر، استفاده کرد:
TimeOnly time = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture); // Custom format Console.WriteLine(time.ToString("T", CultureInfo.InvariantCulture)); // "17:00:00" (long time format)
فرض کنید رکوردی را به صورت زیر تعریف کردهایم که از نوعهای جدید DateOnly و TimeOnly، تشکیل شدهاست:
public record DataTypeTest(DateOnly Date, TimeOnly Time);
var date = DateOnly.FromDateTime(DateTime.Now); var time = TimeOnly.FromDateTime(DateTime.Now); var test = new DataTypeTest(date, time); var json = JsonSerializer.Serialize(test);
Serialization and deserialization of 'System.DateOnly' instances are not supported.
برای رفع این مشکل میتوان ابتدا تبدیلگر ویژهی DateOnly و
public class DateOnlyConverter : JsonConverter<DateOnly> { private readonly string _serializationFormat; public DateOnlyConverter() : this(null) { } public DateOnlyConverter(string? serializationFormat) { _serializationFormat = serializationFormat ?? "yyyy-MM-dd"; } public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); return DateOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture); } public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(_serializationFormat)); }
public class TimeOnlyConverter : JsonConverter<TimeOnly> { private readonly string _serializationFormat; public TimeOnlyConverter() : this(null) { } public TimeOnlyConverter(string? serializationFormat) { _serializationFormat = serializationFormat ?? "HH:mm:ss.fff"; } public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var value = reader.GetString(); return TimeOnly.ParseExact(value!, _serializationFormat, CultureInfo.InvariantCulture); } public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(_serializationFormat)); }
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); jsonOptions.Converters.Add(new DateOnlyConverter()); jsonOptions.Converters.Add(new TimeOnlyConverter()); var json = JsonSerializer.Serialize(test, jsonOptions);
خلاصه اشتراکهای روز دو شنبه 7 آذر 1390
کتابخانه tinymce
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public class MakeTinyMceRequiredAttribute : ValidationAttribute, IClientModelValidator { public MakeTinyMceRequiredAttribute() { ErrorMessage = "لطفا {0} را وارد نمایید"; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var displayName = validationContext.DisplayName; ErrorMessage = ErrorMessage.Replace("{0}", displayName); if (string.IsNullOrWhiteSpace(value?.ToString())) { return new ValidationResult(ErrorMessage); } return ValidationResult.Success; } public void AddValidation(ClientModelValidationContext context) { var displayName = context.ModelMetadata.ContainerMetadata .ModelType.GetProperty(context.ModelMetadata.PropertyName) .GetCustomAttributes(typeof(DisplayAttribute), false) .Cast<DisplayAttribute>() .FirstOrDefault()?.Name; ErrorMessage = ErrorMessage.Replace("{0}", displayName); MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-makeTinyMceRequired", ErrorMessage); } public bool MergeAttribute(IDictionary<string, string> attributes, string key, string value) { if (attributes.ContainsKey(key)) { return false; } attributes.Add(key, value); return true; } }
if (jQuery.validator) { // For hidden inputs $.validator.setDefaults({ ignore: [] }); // makeTinyMceRequired jQuery.validator.addMethod('makeTinyMceRequired', function (value, element, param) { var editorId = $(element).attr('id'); var editorContent = tinyMCE.get(editorId).getContent(); $('body').append(`<div id="test-makeTinyMceRequired">${editorContent}</div>`); var result = isNullOrWhitespace($('#test-makeTinyMceRequired').text()); $('#test-makeTinyMceRequired').remove(); return !result; }); jQuery.validator.unobtrusive.adapters.addBool('makeTinyMceRequired'); } function isNullOrWhitespace(input) { if (typeof input === 'undefined' || input == null) return true; return input.replace(/\s/g, '').length < 1; }
در مورد static reflection مقدمهای پیشتر در این سایت قابل مطالعه است (^) و پیشنیاز بحث جاری است. در ادامه قصد داریم یک سری از کاربردهای متداول آنرا که این روزها در گوشه و کنار وب یافت میشود، به زبان ساده بررسی کنیم.
بهبود کدهای موجود
از static reflection در دو حالت کلی میتوان استفاده کرد. یا قرار است کتابخانهای را از صفر طراحی کنیم یا اینکه خیر؛ کتابخانهای موجود است و میخواهیم کیفیت آنرا بهبود ببخشیم. هدف اصلی هم «حذف رشتهها» و «استفاده از کد بجای رشتهها» است.
برای مثال قطعه کد زیر یک مثال متداول مرتبط با WPF و یا Silverlight است. در آن با پیاده سازی اینترفیس INotifyPropertyChanged و استفاده از متد raisePropertyChanged ، به رابط کاربری برنامه اعلام خواهیم کرد که لطفا خودت را بر اساس اطلاعات جدید تنظیم شده در قسمت set خاصیت Name ، به روز کن:
using System.ComponentModel;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
تعاریف قسمت PropertyChangedEventArgs این پیاده سازی، خارج از کنترل ما است و در دات نت فریم ورک تعریف شده است. حتما هم نیاز به رشته دارد؛ آن هم نام خاصیتی که تغییر کرده است. چقدر خوب میشد اگر میتوانستیم این رشته را حذف کنیم تا کامپایلر بتواند صحت بکارگیری اطلاعات وارد شده را دقیقا پیش از اجرای برنامه بررسی کند. الان فقط در زمان اجرا است که متوجه خواهیم شد، مثلا آیا به روز رسانی مورد نظر صورت گرفتهاست یا خیر؛ اگر نه، یعنی احتمالا یک اشتباه تایپی جایی وجود دارد.
برای بهبود این کد همانطور که در قسمت قبل نیز گفته شد، از ترکیب کلاسهای Expression و Func استفاده خواهیم کرد. در اینجا Func قرار نیست چیزی را اجرا کند، بلکه از آن به عنوان قطعه کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده میشود. این استخراج اطلاعات هم توسط کلاس Expression انجام میشود. بنابراین قسمت اول بهبود کد به صورت زیر شروع میشود:
void raisePropertyChanged(Expression<Func<object>> expression)
الان اگر متد raisePropertyChanged بکارگرفته شده در خاصیت Name را بخواهیم اصلاح کنیم، حداقل با دو واقعهی مطلوب زیر مواجه خواهیم شد:
Intellisense به صورت خودکار کار میکند:
حتی بدویترین ابزارهای Refactoring موجود (منظور همان ابزار توکار VS.NET است!) هم امکان Refactoring را در اینجا فراهم خواهند ساخت:
در پایان کد تکمیل شده فوق به شرح زیر خواهد بود که در آن از کلاس Expression جهت استخراج Member.Name استفاده شده است:
using System;
using System.ComponentModel;
using System.Linq.Expressions;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged(() => Name);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(Expression<Func<object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
}
}
سادهترین تعریف MVVM، نهایت استفاده از امکانات Binding موجود در WPF و Silverlight است. اما خوب، همیشه همه چیز بر وفق مراد نیست. مثلا کنترل WebBrowser را در WPF در نظر بگیرید. فرض کنید که میخواهیم خاصیت Source آنرا در ViewModel مقدار دهی کنیم تا صفحهای را نمایش دهد. بلافاصله با خطای زیر متوقف خواهیم شد:
A 'Binding' cannot be set on the 'Source' property of type 'WebBrowser'.
A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
بله؛ این خاصیت از نوع DependencyProperty نیست و نمیتوان چیزی را به آن Bind کرد. بنابراین این نکته مهم را توسعه دهندههای کنترلهای WPF و Silverlight همیشه باید بخاطر داشته باشند که اگر قرار است کنترلهای شما MVVM friendly باشند باید کمی بیشتر زحمت کشیده و بجای تعریف خواص ساده دات نتی، خواص مورد نظر را از نوع DependencyProperty تعریف کنید.
الان که تعریف نشده چه باید کرد؟
پاسخ متداول آن این است: مهم نیست؛ خودمان میتوانیم اینکار را انجام دهیم! یک Attached property یا به عبارتی یک Behavior را تعریف و سپس به کمک آن عملیات Binding را میسر خواهیم ساخت. برای مثال:
در این Attached property قصد داریم یک خاصیت جدید به نام BindableSource را جهت کنترل WebBrowser تعریف کنیم:
using System;
using System.Windows;
using System.Windows.Controls;
namespace WebBrowserSample.Behaviors
{
public static class WebBrowserBehaviors
{
public static readonly DependencyProperty BindableSourceProperty =
DependencyProperty.RegisterAttached("BindableSource",
typeof(object),
typeof(WebBrowserBehaviors),
new UIPropertyMetadata(null, BindableSourcePropertyChanged));
public static object GetBindableSource(DependencyObject obj)
{
return (string)obj.GetValue(BindableSourceProperty);
}
public static void SetBindableSource(DependencyObject obj, object value)
{
obj.SetValue(BindableSourceProperty, value);
}
public static void BindableSourcePropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
WebBrowser browser = o as WebBrowser;
if (browser == null) return;
Uri uri = null;
if (e.NewValue is string)
{
var uriString = e.NewValue as string;
uri = string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString);
}
else if (e.NewValue is Uri)
{
uri = e.NewValue as Uri;
}
if (uri != null) browser.Source = uri;
}
}
}
یک مثال ساده از استفادهی آن هم به صورت زیر میتواند باشد:
ابتدا ViewModel مرتبط با فرم برنامه را تهیه خواهیم کرد. اینجا چون یک خاصیت را قرار است Bind کنیم، همینجا داخل ViewModel آنرا تعریف کردهایم. اگر تعداد آنها بیشتر بود بهتر است به یک کلاس مجزا مثلا GuiModel منتقل شوند.
using System;
using System.ComponentModel;
namespace WebBrowserSample.ViewModels
{
public class MainWindowViewModel : INotifyPropertyChanged
{
Uri _sourceUri;
public Uri SourceUri
{
get { return _sourceUri; }
set
{
_sourceUri = value;
raisePropertyChanged("SourceUri");
}
}
public MainWindowViewModel()
{
SourceUri = new Uri(@"C:\path\arrow.png");
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
در ادامه بجای استفاده از خاصیت Source که قابلیت Binding ندارد، از Behavior سفارشی تعریف شده استفاده خواهیم کرد. ابتدا باید فضای نام آن تعریف شود، سپس BindableSource مرتبط آن در دسترس خواهد بود:
<Window x:Class="WebBrowserSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:WebBrowserSample.ViewModels"
xmlns:B="clr-namespace:WebBrowserSample.Behaviors"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<VM:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<WebBrowser B:WebBrowserBehaviors.BindableSource="{Binding SourceUri}" />
</Grid>
</Window>
نمونه مشابه این مورد را در مثال «استفاده از کنترلهای Active-X در WPF» پیشتر در این سایت دیدهاید.
در ادامه مطالب مربوط به برنامه نویسی تابعی، قصد دارم بیشتر وارد کد شویم و مباحث عنوان شده را در دنیای کد پیاده سازی کنیم. هدف این قسمت، refactor کردن کد موجود به یک معماری immutable هست. پیشتر درباره immutable ها صحبت کردیم. ابتدا برای یکسان سازی ادبیات مورد استفاده، چند کلمه را مجددا تعریف خواهیم کرد:
- Immutability: عدم توانایی تغییر داده
- State: دادههایی که در طول زمان تغییر میکنند
- Side Effect: تغییری که روی دادهها اتفاق میافتد
در قطعه کد زیر سعی شدهاست تفاوت یک کلاس Stateless و stateful را به سادگی نشان دهیم:
//Stateful public class UserProfile { private User _user; private string _address; public void UpdateUser(int userId, string name) { _user = new User(userId, name); } } //Stateless public class User { public User(int id, string name) { Id = id; Name = name; } public int Id { get; } public string Name { get; } }
چرا Immutable بودن مهم است؟
هر عمل mutable معادل کدی غیر شفاف است. در واقع وابستگی هر عملی که انجام میدهیم به state، باعث میشود که شرایط ناپایداری را در کد داشته باشیم. به طور مثال در یک عملیات چند نخی تصور کنید که چندین نخ به طور همزمان میتوانند state را تغییر دهند و مدیریت این قضیه باعث به وجود آمدن کدهایی ناخوانا و تحمیل پیچیدگی بیشتر به کد خواهد شد.
در واقع انتظار داریم که به ازای یک ورودی بر اساس بدنهی متد، یک خروجی داشته باشیم؛ ولی در واقعیت تاثیری که اجرای متد بر روی state کل کلاس خواهد گذاشت، از دید ما پنهان است و باعث به وجود آمدن مشکلات بعدی خواهد شد. برای مثال قطعه کد بالا را به صورت Honest بازنویسی میکنیم:
public class UserProfile { private readonly User _user; private readonly string _address; public UserProfile(User user,string address) { _user = user; _address = address; } public UserProfile UpdateUser(int userId, string name) { var newUser = new User(userId, name); return new UserProfile(newUser,_address); } } public class User { public User(int id, string name) { Id = id; Name = name; } public int Id { get; } public string Name { get; } }
در این مثال متد UpdateUser به جای void، یک شی از جنس کلاس UserProfile را بر میگرداند. کلاس UserProfile هم برای وهله سازی نیاز به یک شیء از جنس User و Address را دارد. بنابراین مطمئن هستیم که مقدار دهی شدهاند. نکته دیگر در قطعه کد بالا این است که به ازای هر بار فراخوانی متد، یک شیء جدید بدون وابستگی به وهله سازی اشیاء دیگر، برگردانده میشود.
Immutable
بودن باعث میشود:
- خوانایی کد افزایش پیدا کند
- جای واحدی برای Validate کردن داشته باشیم
- به صورت ذاتی Thread Safe باشیم
در مورد محدودیتهایی که در کار با اشیاء Immutable باید در نظر داشته باشیم، میتوان به مصرف بالای رم و سی پی یو، اشاره کرد. در واقع به نسبت حالت mutate، تعداد اشیاء بیشتری ساخته خواهند شد. در فریمورک دات نت برای کار با اشیا immutable امکاناتی در نظر گرفته شده که این هزینه را کاهش میدهند. به طور مثال میتوانیم از کلاس ImmutableList استفاده کنیم و از ایجاد اشیاء اضافهتر و تحمیل بار اضافی به GC جلوگیری کنیم. یک مثال:
//Create Immutable List ImmutableList<string> list = ImmutableList.Create<string>(); ImmutableList<string> list2 = list.Add("Salam"); //Builder ImmutableList<string>.Builder builder = ImmutableList.CreateBuilder<string>(); builder.Add("avali"); builder.Add("dovomi"); builder.Add("sevomi"); ImmutableList<string> immutableList = builder.ToImmutable();
چطور با side effect کنار بیایم؟
یکی از الگوهای رایج برای این کار، مفهوم جدا سازی Command/Query است. به طور ساده تمامی عملیاتی را که تاثیر گذار هستند، به صورت Command در نظر میگیریم. Command ها معمولا هیچ نوعی را بازگشت نمیدهند و همینطور بر عکس این قضیه برای Query ها صادق است. اشتباه رایج درباره این الگو، محدود کردن این الگو به معماریهای خاصی مانند Domain Driven میباشد؛ در صورتیکه الزامی برای رعایت این الگو در سایر معماریها وجود ندارد.
به مثال زیر دقت کنید. سعی کردم قسمتهای Command و Query را از هم جدا کنم:
در واقع هر برنامه میتواند شامل دو قسمت باشد:
قسمتی که در آن منطق تجاری برنامه پیاده سازی میشود و باید به صورت Immutable
باشد که یک خروجی را تولید میکند و قسمت دیگر برنامه که خروجی تولید شده را برای ذخیره
سازی وضعیت سیستم استفاده میکند.
در واقع یک هسته Immutable، ورودی را دریافت کرده و خروجیهای مورد نیاز را تولید میکند و همه اینها در دل یک پوستهMutable پیاده سازی میشوند که ما در اینجا به آن اصطلاحا Mutable Shell میگوییم.
برای مسائلی که در بالا صحبت شد، نمونهای را آماده کردهام. این نمونه به طور ساده یک سیستم مدیریت نوبت است که نوبتها را در فایلی ذخیره و بازیابی میکند ( mutate ) و منطق مربوط به نوبتها و زمان ویزیت آن میتواند به صورت immutable پیاده سازی شود. این کد در دو حالت functional و غیر functional پیاده سازی شده تا به خوبی تفاوت آن را در حالت قبل و بعد از برنامه نویسی تابعی بتوانیم درک کنیم. به جهت خوانایی بیشتر و دسترسی به کدها، آنها را روی گیتهاب قرار داده و شما میتوانید از اینجا سورس کد مورد نظر را بررسی کنید. سعی شده در این مثال تمامی مواردی که در این قسمت ذکر شد را پیاده سازی کنیم. امیدوارم که مطالب مربوط به برنامه نویسی تابعی یا functional programming توانسته باشد دیدگاه جدیدی را به کدهایی که مینویسیم بدهد. در قسمتهای بعدی به مواردی مانند مدیریت exception ها و کار با null ها و ... خواهیم پرداخت.
الف) مقیاس پذیری سمت سرور
در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواستهای کلاینتها تدارک دیده میشوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام میدهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواستهایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه میگردد. یکی از روشهای رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونههای non-blocking است. به این ترتیب ترد پردازش کنندهی این اعمال Async بلافاصله آزاد شده و سرور میتواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.
ب) بالا بردن پاسخ دهی کلاینتها
کلاینتها نیز اگر مدام درخواستهای blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز میتوان از توانمندیهای Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.
و ... تمام اینها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی میکنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانکهای اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کردهاست که در ادامه جزئیات آنرا بررسی خواهیم کرد.
پیشنیازها
برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژههای کلیدی async و await را پشتیبانی میکند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی میکند، دات نت 4 در اینجا قابل استفاده نخواهد بود.
متدهای الحاقی جدید Async در EF 6.x
جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونههای Async آنها نیز اضافه شدهاند:
QueryableExtensions: AllAsync AnyAsync AverageAsync ContainsAsync CountAsync FirstAsync FirstOrDefaultAsync ForEachAsync LoadAsync LongCountAsync MaxAsync MinAsync SingleAsync SingleOrDefaultAsync SumAsync ToArrayAsync ToDictionaryAsync ToListAsync DbSet: FindAsync DbContext: SaveChangesAsync Database: ExecuteSqlCommandAsync
چند مثال
فرض کنید، مدلهای برنامه، رابطهی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } } public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } }
public class MyContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<BlogPost> BlogPosts { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } }
private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user = context.Users.Add(new User { Name = "Vahid" }); context.BlogPosts.Add(new BlogPost { Content = "Test", Title = "Test", User = user }); await context.SaveChangesAsync(cancellationToken); return user; } }
چند نکته جهت یادآوری مباحث Async
- به امضای متد واژهی کلیدی async اضافه شدهاست، زیرا در بدنهی آن از کلمهی کلیدی await استفاده کردهایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمهی Async اضافه شدهاست. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شدهاست.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجیهای توابع نیاز است از نوع Task باشند و آرگومان جنریک آنها، بیانگر نوع مقداری که باز میگردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه میکنید.
نمونهای از نحوهی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر میتواند باشد:
[AsyncTimeout(8000)] public async Task<ActionResult> Index(CancellationToken cancellationToken)
- برای اجرا و دریافت نتیجهی متدهای Async دار EF، نیاز است از واژهی کلیدی await استفاده گردد.
استفاده کننده نیز میتواند متد addUserAsync را به صورت زیر فراخوانی کند:
var user = await addUserAsync(); Console.WriteLine("user id: {0}", user.Id);
شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) user1.Name = "Vahid N."; await context.SaveChangesAsync(cancellationToken); return user1; } } private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) context.Users.Remove(user1); return await context.SaveChangesAsync(cancellationToken); } }
کدهای Async تقلبی!
به قطعه کد ذیل دقت کنید:
public async Task<List<TEntity>> GetAllAsync() { return await Task.Run(() => _tEntities.ToList()); }
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آنها استفاده میشود، کدهای Async تقلبی میگویند! این عملیات هر چند در یک ترد دیگر انجام میشود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده میکنید:
private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { return await context.Users.ToListAsync(cancellationToken); } }
برای مثال پشت صحنهی متد الحاقی SaveChangesAsync به یک چنین متدی ختم میشود:
internal override async Task<long> ExecuteAsync( //... rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); //...
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است:
var connectionString = "........"; var sql = @"......""; var users = new List<User>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new User { Id = reader.GetInt32(0), Name = reader.GetString(1), }; users.Add(user); } } } }
محدودیت پردازش موازی اعمال در EF
در متد ذیل، دو Task غیرهمزمان تعریف شدهاند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آنها را کردهایم:
// multiple operations private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var task1 = context.Users.ToListAsync(cancellationToken); var task2 = context.BlogPosts.ToListAsync(cancellationToken); await Task.WhenAll(task1, task2); // use task1.Result } }
An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll Additional information: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.
چند متد الحاقی SEO
من هم چند متد الحاقی داشتم که مرتبط با SEO اند، البته حتما قابل رشد هست ...
public class SEO { private const char TitleCharSeparator = '-'; //------------------------------------------------- public static string GetTitle(params string[] crumbs) { var maxLenghtTitle = 60; var title = ""; try { foreach (var crumb in crumbs) { title += string.Format(" {0} {1}", TitleCharSeparator, crumb); } title = title.Substring(3); } catch { } title = title.Substring(0, title.Length <= maxLenghtTitle ? title.Length : maxLenghtTitle).Trim(); return title; } //------------------------------------------------- public static string GenerateMetaTag(this string pageTitle, string pageDescription) { var maxLenghtTitle = 60; var maxLenghtDescription = 170; pageTitle = pageTitle.Substring(0, pageTitle.Length <= maxLenghtTitle ? pageTitle.Length : maxLenghtTitle).Trim(); pageDescription = pageDescription.Substring(0, pageDescription.Length <= maxLenghtDescription ? pageDescription.Length : maxLenghtDescription).Trim(); var meta = ""; try { meta += string.Format("<title>{0}</title>\n", pageTitle); meta += string.Format("<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\"/>\n"); meta += string.Format("<meta charset=\"utf-8\"/>\n"); meta += string.Format("<meta name=\"description\" content=\"{0}\"/>\n", pageDescription); meta += string.Format("<meta name=\"robots\" content=\"{0}\" />\n", "follow"); meta += string.Format("<link rel=\"shortcut icon\" href=\"{0}\"/>\n", "~/cdn/images/ui/favicon.ico"); } catch { } return meta; } //------------------------------------------------- public static string GenerateSlug(this string title) { var maxLenghtSlug = 45; string str = title.RemoveAccent().ToLower();
str = Regex.Replace(str, @"[^a-z0-9-\u0600-\u06FF]", "-"); str = Regex.Replace(str, @"\s+", "-").Trim(); str = Regex.Replace(str, @"-+", "-"); str = str.Substring(0, str.Length <= maxLenghtSlug ? str.Length : maxLenghtSlug).Trim();
return str; } //------------------------------------------------- private static string RemoveAccent(this string txt) { var bytes = Encoding.GetEncoding("UTF-8").GetBytes(txt); return Encoding.UTF8.GetString(bytes); } //------------------------------------------------- }
ASP.NET MVC #13
اعتبار سنجی اطلاعات ورودی در فرمهای ASP.NET MVC
زمانیکه شروع به دریافت اطلاعات از کاربران کردیم، نیاز خواهد بود تا اعتبار اطلاعات ورودی را نیز ارزیابی کنیم. در ASP.NET MVC، به کمک یک سری متادیتا، نحوهی اعتبار سنجی، تعریف شده و سپس فریم ورک بر اساس این ویژگیها، به صورت خودکار اعتبار اطلاعات انتساب داده شده به خواص یک مدل را در سمت کلاینت و همچنین در سمت سرور بررسی مینماید.
این ویژگیها در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند که به صورت پیش فرض در هر پروژه جدید ASP.NET MVC لحاظ میشود.
یک مثال کاربردی
مدل زیر را به پوشه مدلهای یک پروژه جدید خالی ASP.NET MVC اضافه کنید:
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Models
{
public class Customer
{
public int Id { set; get; }
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
public string Name { set; get; }
[Display(Name = "Email address")]
[Required(ErrorMessage = "Email address is required.")]
[RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
ErrorMessage = "Please enter a valid email address.")]
public string Email { set; get; }
[Range(0, 10)]
[Required(ErrorMessage = "Rating is required.")]
public double Rating { set; get; }
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { set; get; }
}
}
سپس کنترلر جدید زیر را نیز به برنامه اضافه نمائید:
using System.Web.Mvc;
using MvcApplication9.Models;
namespace MvcApplication9.Controllers
{
public class CustomerController : Controller
{
[HttpGet]
public ActionResult Create()
{
var customer = new Customer();
return View(customer);
}
[HttpPost]
public ActionResult Create(Customer customer)
{
if (this.ModelState.IsValid)
{
//todo: save data
return Redirect("/");
}
return View(customer);
}
}
}
بر روی متد Create کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه Create a strongly typed view را انتخاب کرده و مدل را Customer انتخاب کنید. همچنین قالب Scaffolding را نیز بر روی Create قرار دهید.
توضیحات تکمیلی
همانطور که در مدل برنامه ملاحظه مینمائید، به کمک یک سری متادیتا یا اصطلاحا data annotations، تعاریف اعتبار سنجی، به همراه عبارات خطایی که باید به کاربر نمایش داده شوند، مشخص شده است. ویژگی Required مشخص میکند که کاربر مجبور است این فیلد را تکمیل کند. به کمک ویژگی StringLength، حداکثر تعداد حروف قابل قبول مشخص میشود. با استفاده از ویژگی RegularExpression، مقدار وارد شده با الگوی عبارت باقاعده مشخص گردیده، مقایسه شده و در صورت عدم تطابق، پیغام خطایی به کاربر نمایش داده خواهد شد. به کمک ویژگی Range، بازه اطلاعات قابل قبول، مشخص میگردد.
ویژگی دیگری نیز به نام System.Web.Mvc.Compare مهیا است که برای مقایسه بین مقادیر دو خاصیت کاربرد دارد. برای مثال در یک فرم ثبت نام، عموما از کاربر درخواست میشود که کلمه عبورش را دوبار وارد کند. ویژگی Compare در یک چنین مثالی کاربرد خواهد داشت.
در مورد جزئیات کنترلر تعریف شده در قسمت 11 مفصل توضیح داده شد. برای مثال خاصیت this.ModelState.IsValid مشخص میکند که آیا کارmodel binding موفق بوده یا خیر و همچنین اعتبار سنجیهای تعریف شده نیز در اینجا تاثیر داده میشوند. بنابراین بررسی آن پیش از ذخیره سازی اطلاعات ضروری است.
در حالت HttpGet صفحه ورود اطلاعات به کاربر نمایش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دریافت میگردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به کاربر مجددا نمایش داده خواهد شد تا فرم پاک نشود و بتواند آنها را اصلاح کند.
برنامه را اجرا کنید. با مراجعه به مسیر http://localhost/customer/create، صفحه ورود اطلاعات کاربر نمایش داده خواهد شد. در اینجا برای مثال در قسمت ورود اطلاعات آدرس ایمیل، مقدار abc را وارد کنید. بلافاصله خطای اعتبار سنجی عدم اعتبار مقدار ورودی نمایش داده میشود. یعنی فریم ورک، اعتبار سنجی سمت کاربر را نیز به صورت خودکار مهیا کرده است.
اگر علاقمند باشید که صرفا جهت آزمایش، اعتبار سنجی سمت کاربر را غیرفعال کنید، به فایل web.config برنامه مراجعه کرده و تنظیم زیر را تغییر دهید:
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
البته این تنظیم تاثیر سراسری دارد. اگر قصد داشته باشیم که این تنظیم را تنها به یک view خاص اعمال کنیم، میتوان از متد زیر کمک گرفت:
@{ Html.EnableClientValidation(false); }
در این حالت اگر مجددا برنامه را اجرا کرده و اطلاعات نادرستی را وارد کنیم، باز هم همان خطاهای تعریف شده، به کاربر نمایش داده خواهد شد. اما اینبار یکبار رفت و برگشت اجباری به سرور صورت خواهد گرفت، زیرا اعتبار سنجی سمت کاربر (که درون مرورگر و توسط کدهای جاوا اسکریپتی اجرا میشود)، غیرفعال شده است. البته امکان غیرفعال کردن جاوا اسکریپت توسط کاربر نیز وجود دارد. به همین جهت بررسی خودکار سمت سرور، امنیت سیستم را بهبود خواهد بخشید.
نحوه تعریف عناصر مرتبط با اعتبار سنجی در Viewهای برنامه نیز به شکل زیر است:
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Customer</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
همانطور که ملاحظه میکنید به صورت پیش فرض از jQuery validator در سمت کلاینت استفاده شده است. فایل jquery.validate.unobtrusive متعلق به تیم ASP.NET MVC است و کار آن وفق دادن سیستم موجود، با jQuery validator میباشد (validation adapter). در نگارشهای قبلی، از کتابخانههای اعتبار سنجی مایکروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان کتابخانه برگزیده مطرح است.
Unobtrusive همچنین در اینجا به معنای مجزا سازی کدهای جاوا اسکریپتی، از سورس HTML صفحه و استفاده از ویژگیهای data-* مرتبط با HTML5 برای معرفی اطلاعات مورد نیاز اعتبار سنجی است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />
اگر خواستید این مساله را بررسی کنید، فایل web.config قرار گرفته در ریشه اصلی برنامه را باز کنید. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false کرده و بار دیگر برنامه را اجرا کنید. در این حالت کلیه کدهای اعتبار سنجی، به داخل سورس View رندر شده، تزریق میشوند و مجزا از آن نخواهند بود.
نحوهی تعریف این اسکریپتها نیز جالب توجه است. متد Url.Content، یک متد سمت سرور میباشد که در زمان اجرای برنامه، مسیر نسبی وارد شده را بر اساس ساختار سایت اصلاح میکند. حرف ~ بکارگرفته شده، در ASP.NET به معنای ریشه سایت است. بنابراین مسیر نسبی تعریف شده از ریشه سایت شروع و تفسیر میشود.
اگر از این متد استفاده نکنیم، مجبور خواهیم شد که مسیرهای نسبی را به شکل زیر تعریف کنیم:
<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>
در این حالت بسته به محل قرارگیری صفحات و همچنین برنامه در سایت، ممکن است آدرس فوق صحیح باشد یا خیر. اما استفاده از متد Url.Content، کار مسیریابی نهایی را خودکار میکند.
البته اگر به فایل Views/Shared/_Layout.cshtml، مراجعه کنید، تعریف و الحاق کتابخانه اصلی jQuery در آنجا انجام شده است. بنابراین میتوان این دو تعریف دیگر مرتبط با اعتبار سنجی را به آن فایل هم منتقل کرد تا همهجا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهای اعتبار سنجی مدل که به صورت دستی اضافه شده باشند نمایش داده میشود. این مورد در قسمت 11 توضیح داده شد (چون پارامتر آن true وارد شده، فقط خطاهای سطح مدل را نمایش میدهد).
متد Html.ValidationMessageFor، با توجه به متادیتای یک خاصیت و همچنین استثناهای صادر شده حین model binding خطایی را به کاربر نمایش خواهد داد.
اعتبار سنجی سفارشی
ویژگیهای اعتبار سنجی از پیش تعریف شده، پر کاربردترینها هستند؛ اما کافی نیستند. برای مثال در مدل فوق، StartDate نباید کمتر از سال 2000 وارد شود و همچنین در آینده هم نباید باشد. این موارد اعتبار سنجی سفارشی را چگونه باید با فریم ورک، یکپارچه کرد؟
حداقل دو روش برای حل این مساله وجود دارد:
الف) نوشتن یک ویژگی اعتبار سنجی سفارشی
ب) پیاده سازی اینترفیس IValidatableObject
تعریف یک ویژگی اعتبار سنجی سفارشی
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute
{
public int MinYear { set; get; }
public override bool IsValid(object value)
{
if (value == null) return false;
var date = (DateTime)value;
if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
return false;
return true;
}
}
}
برای نوشتن یک ویژگی اعتبار سنجی سفارشی، با ارث بری از کلاس ValidationAttribute شروع میکنیم. سپس باید متد IsValid آنرا تحریف کنیم. اگر این متد false برگرداند به معنای شکست اعتبار سنجی میباشد.
در ادامه برای بکارگیری آن خواهیم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }
اکنون مجددا برنامه را اجرا نمائید. اگر تاریخ غیرمعتبری وارد شود، اعتبار سنجی سمت سرور رخ داده و سپس نتیجه به کاربر نمایش داده میشود.
اعتبار سنجی سفارشی به کمک پیاده سازی اینترفیس IValidatableObject
یک سؤال: اگر اعتبار سنجی ما پیچیدهتر باشد چطور؟ مثلا نیاز باشد مقادیر دریافتی چندین خاصیت با هم مقایسه شده و سپس بر این اساس تصمیم گیری شود. برای حل این مشکل میتوان از اینترفیس IValidatableObject کمک گرفت. در این حالت مدل تعریف شده باید اینترفیس یاد شده را پیاده سازی نماید. برای مثال:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MvcApplication9.CustomValidators;
namespace MvcApplication9.Models
{
public class Customer : IValidatableObject
{
//... same as before
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var fields = new[] { "StartDate" };
if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
yield return new ValidationResult("Please enter a valid date.", fields);
if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
yield return new ValidationResult("Accepted date should be greater than 2003", fields);
}
}
}
در اینجا در متد Validate، فرصت خواهیم داشت تا به مقادیر کلیه خواص تعریف شده در مدل دسترسی پیدا کرده و بر این اساس اعتبار سنجی بهتری را انجام دهیم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، کافی است توسط yield return new ValidationResult، یک پیغام را به همراه فیلدهایی که باید این پیغام را نمایش دهند، بازگردانیم.
به این نوع مدلها، self validating models هم گفته میشود.
یک نکته:
از MVC3 به بعد، حین کار با ValidationAttribute، امکان تحریف متد IsValid به همراه پارامتری از نوع ValidationContext نیز وجود دارد. به این ترتیب میتوان به اطلاعات سایر خواص نیز دست یافت. البته در این حالت نیاز به استفاده از Reflection خواهد بود و پیاده سازی IValidatableObject، طبیعیتر به نظر میرسد:
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var info = validationContext.ObjectType.GetProperty("Rating");
//...
return ValidationResult.Success;
}
فعال سازی سمت کلاینت اعتبار سنجیهای سفارشی
اعتبار سنجیهای سفارشی تولید شده تا به اینجا، تنها سمت سرور است که فعال میشوند. به عبارتی باید یکبار اطلاعات به سرور ارسال شده و در بازگشت، نتیجه عملیات به کاربر نمایش داده خواهد شد. اما ویژگیهای توکاری مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت کاربر هم فعال هستند و اگر جاوا اسکریپت در مرورگر کاربر غیرفعال نشده باشد، نیازی به ارسال اطلاعات یک فرم به سرور جهت اعتبار سنجی اولیه، نخواهد بود.
در اینجا باید سه مرحله برای پیاده سازی اعتبار سنجی سمت کلاینت طی شود:
الف) ویژگی سفارشی اعتبار سنجی تعریف شده باید اینترفیس IClientValidatable را پیاده سازی کند.
ب) سپس باید متد jQuery validation متناظر را پیاده سازی کرد.
ج) و همچنین مانند تیم ASP.NET MVC، باید unobtrusive adapter خود را نیز پیاده سازی کنیم. به این ترتیب متادیتای ASP.NET MVC به فرمتی که افزونه jQuery validator آنرا درک میکند، وفق داده خواهد شد.
در ادامه، تکمیل کلاس سفارشی MyDateValidator را ادامه خواهیم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute, IClientValidatable
{
// ... same as before
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "mydatevalidator",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
yield return rule;
}
}
}
در اینجا نحوه پیاده سازی اینترفیس IClientValidatable را ملاحظه مینمائید. ValidationType، نام متدی خواهد بود که در سمت کلاینت، کار بررسی اعتبار دادهها را به عهده خواهد گرفت.
سپس برای مثال یک فایل جدید به نام customvaildation.js به پوشه اسکریپتهای برنامه با محتوای زیر اضافه خواهیم کرد:
/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />
jQuery.validator.addMethod("mydatevalidator",
function (value, element, param) {
return Date.parse(value) < new Date();
});
jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");
توسط referenceهایی که مشاهده میکنید، intellisense جیکوئری در VS.NET فعال میشود.
سپس به کمک متد jQuery.validator.addMethod، همان مقدار ValidationType پیشین را معرفی و در ادامه بر اساس مقدار value دریافتی، تصمیم گیری خواهیم کرد. اگر خروجی false باشد، به معنای شکست اعتبار سنجی است.
همچنین توسط متد jQuery.validator.unobtrusive.adapters.addBool، این متد جدید را به مجموعه وفق دهندهها اضافه میکنیم.
و در آخر این فایل جدید باید به View مورد نظر یا فایل master page سیستم اضافه شود:
<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>
تغییر رنگ و ظاهر پیغامهای اعتبار سنجی
اگر از رنگ پیش فرض قرمز پیغامهای اعتبار سنجی خرسند نیستید، باید اندکی CSS سایت را ویرایش کرد که شامل اعمال تغییرات به موارد ذیل خواهد شد:
1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid
نحوه جدا سازی تعاریف متادیتا از کلاسهای مدل برنامه
فرض کنید مدلهای برنامه شما به کمک یک code generator تولید میشوند. در این حالت هرگونه ویژگی اضافی تعریف شده در این کلاسها پس از تولید مجدد کدها از دست خواهند رفت. به همین منظور امکان تعریف مجزای متادیتاها نیز پیش بینی شده است:
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
class CustomerMetadata
{
}
}
public partial class Customer : IValidatableObject
{
حالت کلی روش انجام آن هم به شکلی است که ملاحظه میکنید. کلاس اصلی، به صورت partial معرفی خواهد شد. سپس کلاس partial دیگری نیز به همین نام که در برگیرنده یک کلاس داخلی دیگر برای تعاریف متادیتا است، به پروژه اضافه میگردد. به کمک ویژگی MetadataType، کلاسی که قرار است ویژگیهای خواص از آن خوانده شود، معرفی میگردد. موارد عنوان شده، شکل کلی این پیاده سازی است. برای نمونه اگر با WCF RIA Services کار کرده باشید، از این روش زیاد استفاده میشود. کلاس خصوصی تو در توی تعریف شده صرفا وظیفه ارائه متادیتاهای تعریف شده را به فریم ورک خواهد داشت و هیچ کاربرد دیگری ندارد.
در ادامه کلیه خواص کلاس Customer به همراه متادیتای آنها باید به کلاس CustomerMetadata منتقل شوند. اکنون میتوان تمام متادیتای کلاس اصلی Customer را حذف کرد.
اعتبار سنجی از راه دور (remote validation)
فرض کنید شخصی مشغول به پر کردن فرم ثبت نام، در سایت شما است. پس از اینکه نام کاربری دلخواه خود را وارد کرد و مثلا به فیلد ورود کلمه عبور رسید، در همین حال و بدون ارسال کل صفحه به سرور، به او پیغام دهیم که نام کاربری وارد شده، هم اکنون توسط شخص دیگری در حال استفاده است. این مکانیزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و یک درخواست Ajaxایی خودکار را به سرور ارسال خواهد کرد و نتیجه نهایی را به کاربر نمایش میدهد؛ کارهایی که به سادگی توسط کدهای جاوا اسکریپتی قابل مدیریت نیستند و نیاز به تعامل با سرور، در این بین وجود دارد. پیاده سازی آن هم به نحو زیر است:
برای مثال خاصیت Name را در مدل برنامه به نحو زیر تغییر دهید:
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
controller: "Customer",
AdditionalFields = "Email",
HttpMethod = "POST",
ErrorMessage = "Username is not available.")]
public string Name { set; get; }
سپس متد زیر را نیز به کنترلر Customer اضافه کنید:
[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
if (name.ToLowerInvariant() == "vahid") return Json(false);
if (email.ToLowerInvariant() == "name@site.com") return Json(false);
//...
return Json(true);
}
توضیحات:
توسط ویژگی System.Web.Mvc.Remote، نام کنترلر و متدی که در آن قرار است به صورت خودکار توسط jQuery Ajax فراخوانی شود، مشخص خواهند شد. همچنین اگر نیاز بود فیلدهای دیگری نیز به این متد کنترلر ارسال شوند، میتوان آنها را توسط خاصیت AdditionalFields، مشخص کرد.
سپس در کدهای کنترلر مشخص شده، متدی با پارامترهای خاصیت مورد نظر و فیلدهای اضافی دیگر، تعریف میشود. در اینجا فرصت خواهیم داشت تا برای مثال پس از بررسی بانک اطلاعاتی، خروجی Json ایی را بازگردانیم. return Json false به معنای شکست اعتبار سنجی است.
توسط ویژگی OutputCache، از کش شدن نتیجه درخواستهای Ajaxایی جلوگیری کردهایم. همچنین نوع درخواست هم جهت امنیت بیشتر، به HttpPost محدود شده است.
تمام کاری که باید انجام شود همین مقدار است و مابقی مسایل مرتبط با اعمال و پیاده سازی آن خودکار است.
استفاده از مکانیزم اعتبار سنجی مبتنی برمتادیتا در خارج از ASP.Net MVC
مباحثی را که در این قسمت ملاحظه نمودید، منحصر به ASP.NET MVC نیستند. برای نمونه توسط متد الحاقی زیر نیز میتوان یک مدل را مثلا در یک برنامه کنسول هم اعتبار سنجی کرد. بدیهی است در این حالت نیاز خواهد بود تا ارجاعی را به اسمبلی System.ComponentModel.DataAnnotations، به برنامه اضافه کنیم و تمام عملیات هم دستی است و فریم ورک ویژهای هم وجود ندارد تا یک سری از کارها را به صورت خودکار انجام دهد.
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Helper
{
public static class ValidationHelper
{
public static bool TryValidateObject(this object instance)
{
return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
}
}
}