آموزش MVC
گاهی از اوقات نیاز است تا از یک محتوای پویا، برای تولید فایلهای CSS و اسکریپتهای خود استفاده کنید. دلایل زیادی برای اینکار وجود دارند؛ همانند اسکریپت تولید شده در Signalr که بر اساس کلاس hub شما و متدهای پیاده سازی شدهی در آن تولید میشود. همچنین روشهای زیادی برای تولید این محتوای پویا وجود دارد که یک نمونهی آن در اینجا ذکر شده است.
قرار دادن این محتوای تولید شده در سیستم Bundling MVC به شکل مستقیم امکان پذیر نیست؛ زیرا این سیستم با فایلهای استاتیک سر و کار دارد و افزودن یک url به آن مجاز نمیباشد. حال اگر در پروژهی خود محتوای پویایی را تولید کرده و میخواهید از مزایای فشرده سازی سیستم Bundling بهرهمند شوید، باید مراحل زیر را انجام دهید:
ابتدا متدی را برای دریافت محتوای تولید شده بنویسید. برای مثال برای دریافت محتوای تولیده شدهی فایل hubs.js میتوانید از متد زیر استفاده کنید:
|
1
2
3
4
5
6
|
public static string GetSignalRContent(){ var resolver = new DefaultHubManager(new DefaultDependencyResolver()); var proxy = new DefaultJavaScriptProxyGenerator(resolver, new NullJavaScriptMinifier()); return proxy.GenerateProxy("/signalr");} |
سپس باید سیستم مسریابی پیش فرض سیستم Bundling را با سیستم مسیریابی سفارشی خود جایگزین کنید. کاری که باید انجام دهید اینست که در سیستم مسیریابی سفارشی خود چک کنید اگر مسیر درخواستی به مسیر مورد نظر شما اشاره دارد، مقدار true را برگشت دهید. در واقع در سیستم مسیریابی پیش فرض اگر فایلی بطور فیزیکی وجود نداشته باشد، مقدار برگشتی false خواهد بود.
همچنین در این سیستم مسیریابی سفارشی شما باید محتوای تولید شده را هم در اختیار داشته باشید. برای نمونه به کد زیر توجه کنید که ما از کلاس VirtualPathProvider، یک کلاس مشتق کرده و سیستم مسیریابی دلخواه خود را ایجاد میکنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
public class CustomVirtualPathProvider : VirtualPathProvider{ public CustomActionVirtualPathProvider(VirtualPathProvider virtualPathProvider) { // Wrap an existing virtual path provider VirtualPathProvider = virtualPathProvider; } protected VirtualPathProvider VirtualPathProvider { get; set; } public override string CombineVirtualPaths(string basePath, string relativePath) { return VirtualPathProvider.CombineVirtualPaths(basePath, relativePath); } public override bool DirectoryExists(string virtualDir) { return VirtualPathProvider.DirectoryExists(virtualDir); } public override bool FileExists(string virtualPath) { if (virtualPath == "~/signalr/hubs") { return true; } return VirtualPathProvider.FileExists(virtualPath); } public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { // BaseClass can't create a CacheDependency for your content, remove it // You could also add your own CacheDependency and aggregate it with the base dependency List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList(); virtualPathDependenciesCopy.Remove("~/signalr/hubs"); return VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy, utcStart); } public override string GetCacheKey(string virtualPath) { return VirtualPathProvider.GetCacheKey(virtualPath); } public override VirtualDirectory GetDirectory(string virtualDir) { return VirtualPathProvider.GetDirectory(virtualDir); } public override VirtualFile GetFile(string virtualPath) { if (virtualPath == "~/signalr/hubs") { return new CustomVirtualFile(virtualPath, new MemoryStream(Encoding.Default.GetBytes(GetSignalRContent()))); } return VirtualPathProvider.GetFile(virtualPath); } public override string GetFileHash(string virtualPath, IEnumerable virtualPathDependencies) { return VirtualPathProvider.GetFileHash(virtualPath, virtualPathDependencies); } public override object InitializeLifetimeService() { return VirtualPathProvider.InitializeLifetimeService(); }}public class CustomVirtualFile : VirtualFile{ public CustomVirtualFile (string virtualPath, Stream stream) : base(virtualPath) { Stream = stream; } public Stream Stream { get; private set; } public override Stream Open() { return Stream; }} |
ابتدا در بازنویسی متد FileExists باید چک کنیم اگر مسیر درخواستی به مسیر مورد نظر ما اشاره داشت، منطق خود را پیاده کنیم:
|
1
2
3
4
5
6
7
8
9
|
public override bool FileExists(string virtualPath){ if (virtualPath == "~/signalr/hubs") { return true; } return VirtualPathProvider.FileExists(virtualPath);} |
در این متد اگر مسیر درخواستی مطابق مسیر مورد نظر ما بود باید true برگشت دهیم، در غیر اینصورت کار را به کلاس پایه میسپاریم.
همچنین باید توجه داشت که کلاس پایه، قادر به تولید CacheDependency برای محتوای تولیدی شده ما نیست. بنابراین باید متد GetCacheDependency کلاس پایهی ما بازنویسی و منطق مورد نظر را برای آن پیاده سازی کنیم. در این متد ابتدا مسیر مورد نظر خود را از لیست مسیرهایی که باید از CacheDependency استفاده کنند، حذف و سپس سناریوی خود را پیاده سازی میکنیم. در این مثال من از پیاده سازی آن خودداری کرده و فقط مسیر را از لیست مسیرها حذف کردهام:
|
1
2
3
4
5
6
7
|
public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart){ List<string> virtualPathDependenciesCopy = virtualPathDependencies.Cast<string>().ToList(); virtualPathDependenciesCopy.Remove("~/signalr/hubs"); return VirtualPathProvider.GetCacheDependency(virtualPath, virtualPathDependenciesCopy, utcStart);} |
سپس نوبت دریافت محتوای تولید شدهی پویا است که باید با بازنویسی متد GetFile منطق خود را اعمال کنیم:
|
1
2
3
4
5
6
7
8
9
10
|
public override VirtualFile GetFile(string virtualPath){ if (virtualPath == "~/signalr/hubs") { return new CustomVirtualFile(virtualPath, new MemoryStream(Encoding.Default.GetBytes(GetSignalRContent()))); } return VirtualPathProvider.GetFile(virtualPath);} |
در این متد ابتدا چک میکنیم اگر مسیر درخواست شده به مسیر مورد نظر ما اشاره داشت، با استفاده از متد GetSignalRContent محتوای تولید شدهی پویا را دریافت و از طریق کلاس CustomVirtualFile که از کلاس VirtualFile مشتق کرده ایم، آنرا باز میگردانیم. کار این کلاس هم فراهم کردن بستری برای اشیایی است که یک فایل فیزیکی را در قالب سیستم فایل مجازی بکار میگیرند. تنها نکتهی قابل توجه در این کلاس، بازنویسی متد Open کلاس پایه، برای بازگرداندن یک Stream فقط خواندنی از محتوای منبع مورد نظر ماست. از آنجا که منبع ما در اینجا به طور پویا تولید میشود، همانطور که دیدید ما در متد GetFile از یک MemoryStream استفاده کردیم؛ در صورتیکه اگر با یک فایل فیزیکی سر و کار داشتیم، ممکن بود از یک FileStream استفاده کنیم.
در نهایت باید کلاس سفارشی شده را با سیستم مسیریابی پیش فرض سیستم Bundling جایگزین کنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
public static void RegisterBundles(BundleCollection bundles){ BundleTable.VirtualPathProvider = new CustomVirtualPathProvider(BundleTable.VirtualPathProvider); Bundle include = new Bundle("~/bundle") .Include("~/Content/static.js") .Include("~/signalr/hubs"); bundles.Add(include);} |
پس از معرفی ویژگیهای لازم، در ادامه با نحوهی تبدیل این ویژگیها به معادل نگاشت آنها در automapper خواهم پرداخت.
متد زیر هستهی اصلی عملیات است و کلیهی نگاشتهای لازم را انجام میدهد. این متد وظیفهی تبدیل نگاشتها را دارد. نگاشتهایی که با Attributes مشخص شدهاند:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public static void Initialize(Assembly assembly){ //register global convertors. AutoMapper.Mapper.CreateMap<DateTime, string>().ConvertUsing<DateTimeToPersianDateTimeConverter>(); var typesToMap = from t in assembly.GetTypes() let attr = t.GetCustomAttribute<MapFromAttribute>() where attr != null select new {SourceType = attr.SourceType, Destination = t, Attribute = attr}; foreach (var map in typesToMap) { AutoMapper.Mapper.CreateMap(map.SourceType, map.Destination) .DoMapForMemberAttribute() // for different property names in source and destination .DoIgnoreMapAttribute()// ignore specified properties .DoUseValueResolverAttribute()// set value resolvers .DoIgnoreAllNonExisting()// its have to be the latest. ; } //endeach AutoMapper.Mapper.AssertConfigurationIsValid();} |
ورودی این متد اسملبی مربوط به ویوومدل میباشد (برای زمانیکه ویوومدلها در اسمبلی دیگری باشند).
در سطر اول، اقدام به رجیستر کردن کلیهی مبدلهای سراسری میکنیم. در این سطر مبدل تاریخ به کوچی خورشیدی مورد استفاده قرار گرفته است. سپس در اسمبلی داده شده، کلیه نوعهایی که ویژگی MapFromAttribute را دارند، یافته و جدا میکنیم. در حلقهی foreach ابتدا نگاشت نوع مبدأ و مقصد را انجام میدهیم. خروجی این متد از نوع IMappingExpression است. گر چه این اینترفیس برای تغییر بسته است، ولی قابل توسعه میباشد و عملیات را توسط متدهای الحاقی انجام میدهیم(اصل OCP).
اگر به نحوهی نامگذاری متدهای الحاقی تعریف شده دقت کرده باشید، تنها کلمهی Do به ابتدای نام ویژگیها اضافه شده است.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static IMappingExpression DoMapForMemberAttribute(this IMappingExpression expression){ var ok = from p in expression.TypeMap.DestinationType.GetProperties() let attr = p.GetCustomAttribute<MapForMemberAttribute>() where attr != null select new {AttributeValue = attr, PropertyName = p.Name}; foreach (var property in ok) { expression.ForMember(property.PropertyName, opt => opt.MapFrom(property.AttributeValue.MemberToMap)); } return expression;} |
هر IMappingExpression دارای امکاناتی برای نگهداری و انجام فعالیت بر روی یک نگاشت میباشد. در کوئری ابتدای متد، کلیهی پروپرتیهایی را که دارای ویژگی MapForMemeberAttribute میباشند، یافته و جدا میکنیم. این پروپرتیها از نظر معادل اسمی در نوع مبدأ و مقصد متفاوت هستند. سپس در حلقه، کار اتصال پروپرتی مبدأ و مقصد صورت میگیرد.
|
1
2
3
4
5
6
7
8
9
10
|
public static IMappingExpression DoIgnoreAttribute(this IMappingExpression expression){ foreach (var property in expression.TypeMap.DestinationType.GetProperties() .Where(x => x.GetCustomAttribute<IgnoreMapAttribute>() != null)) { expression.ForMember(property.Name, opt => opt.Ignore()); } return expression;} |
این متد کلیهی پروپرتیهایی را که دارای ویژگی IgnoreMapAttribute باشند، از گردونهی نگاشت automapper خارج میکند. به عنوان مثال پروپرتی Password در ویوومدل مربوط به تغییر گذرواژه را نظر بگیرید. این پروپرتی نباید مقدار معادلی در شیء EF داشته باشد. از طرفی هم باید در ویوو وجودداشته باشد. با استفاده از این ویژگی هیچ نگاشتی انجام نمیشود و میتوان تضمین کرد که گذرواژه به ویوومدل و ویوو راه پیدا نمیکند.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static IMappingExpression DoUseValueResolverAttribute(this IMappingExpression expression){ var ok = from p in expression.TypeMap.DestinationType.GetProperties() let attr = p.GetCustomAttribute<UseValueResolverAttribute>() where attr != null select new {AttributeValue = attr, PropertyName = p.Name}; foreach (var property in ok) { expression.ForMember(property.PropertyName, opt => opt.ResolveUsing(property.AttributeValue.ValueResolver)); } return expression;} |
به شیوهی قبل، ابتدا نوع هایی را که دارای ویژگی UseValueResolverAttribute باشند، یافته و جدا میکنیم. سپس در حلقه، کار نگاشت متناظر در automapper انجام میگیرد. لازم به ذکر است که متد opt.ResolveUsing یک شیء با کارآیی (can do) اینترفیس IValueResolver را به عنوان آرگومان میگیرد.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public static IMappingExpression DoIgnoreAllNonExisting(this IMappingExpression expression){ var attr = expression.TypeMap.DestinationType.GetCustomAttribute<MapFromAttribute>(); if (attr?.IgnoreAllNonExistingProperty == false)//instead of if(attr == null || attr.IgnoreAllNonExistingProperty == false) return expression; foreach (var property in expression.TypeMap.GetUnmappedPropertyNames()) { expression.ForMember(property, opt => opt.Ignore()); } return expression;} |
این متد برحسب پرچم تعیین شده در هنگام بکارگیری ویژگی MapFromAttribute رفتار میکند. به این صورت که اگر موقع تعریف، مقدار IgnoreAllNonExistingProperty را صحیح اعلام کنیم، تمام پروپرتیهای مقصد را که معادل اسمی در مبدأ نداشته باشند و همچنین هیچگونه تنظیمی جهت مشخص سازی تکلیف نگاشت آنها صورت نگرفته باشد، از گردونهی نگاشت Automapper خارج میکند.
پس از تنظیم کلیهی نگاشتها در automapper جهت اطمینان از صحت تنظیمات، فراخوانی متد AutoMapper.Mapper.AssertConfigurationIsValid الزامی است. یکی از عواملی که باعث شکست این متد میشود، وجود پروپرتیهایی در نوع مقصد است، بطوریکه معادل اسمی در نوع مبدأ نداشته باشند و یا تنظیمی جهت مشخص سازی نگاشت آن انجام نشده باشد (پروپرتی که قابل نگاشت نباشد). در حقیقت این شکست بسیار مفید است. به این صورت که اگر این شکست صورت نگیرد در حین نگاشت مقادیر، باید از null یا مقدار default بدون اطلاع برنامه نویس برای مقداردهی پروپرتی استفاده کند و این یک حالت نامعلوم شیء است. اگر میخواهید این پروپرتیها مقدار پیشفرضی بگیرند و همچنین باعث شکست عملیات هم نشوند، باید بطور صریح این موضوع را اعلام کنید. این اعلام یا باید به همین روش صورت بگیرد یا باید از ویژگی IgnorMapAttribute استفاده شود. تنها تفاوت این دو، نحوهی اعمال تنظیم میباشد. IgnorMapAttribute باید روی تک تک پروپرتیهای مدنظر قرار گیرد، ولی در روش اول تنها کافیست که مقدار true تنظیم گردد. بهنظر استفاده از IgnoreMapAttribute باعث طولانی شدن کدها میشود؛ اما توصیه میشود که از همین شیوه استفاده کنید.
تا اینجا کدهای مورد نیاز نوشته شدند. در ادامه به ارائهی یک مثال برای نگاشت اشیاء در Automapper توسط Attributeها میپردازم.
مدل سادهی زیر را در نظر بگیرید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public class Student{ public virtual int Id { set; get; } public virtual string Name { set; get; } public virtual string Family { set; get; } public virtual string Email { set; get; } public virtual DateTime RegisterDateTime { set; get; } public virtual ICollection<Book> Books { set; get; }}public class Book{ public virtual int Id { set; get; } public virtual string Name { set; get; } public virtual DateTime BorrowDateTime { set; get; } public virtual DateTime ExpiredDateTime { set; get; } public virtual decimal Price { set; get; } [ForeignKey("StudentIdFk")] public virtual Student Student { set; get; } public virtual int StudentIdFk { set; get; }} |
با ویوومدل متناظر ذیل:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
[MapFrom(typeof (Student), ignoreAllNonExistingProperty: true, alsoCopyMetadata: true)]public class AdminStudentViewModel{ // [IgnoreMap] public int Id { set; get; } [MapForMember("Name")] public string FirstName { set; get; } [MapForMember("Family")] public string LastName { set; get; } [IgnoreMap] public string Email { set; get; } [MapForMember("RegisterDateTime")] public string RegisterDateTimePersian { set; get; } [UseValueResolver(typeof (BookCountValueResolver))] public int BookCounts { set; get; } [UseValueResolver(typeof (BookPriceValueResolver))] public decimal TotalBookPrice { set; get; }}; |
در تنظیم ویژگی MapFromAttribute ابتدا نوع مبدأ (Student) را مشخص کردیم و بعد صراحتاً گفتیم که از نگاشت پروپرتیهای بلاتکلیف صرف نظر کند و همچنین پرچم انتقال Data Annotationهای EF به ویوومدل را هم برافراشتیم. توسط MapForMember پروپرتی FirstName را به پروپرتی Name در مبدأ تنظیم کردیم و LastName را به Family. همچنین Email را بصورت صریح از نگاشت شدن منع کردیم. پروپرتی BookCounts تعداد کتابها را محاسبه میکند و TotalBookPrice قیمت کلیهی کتابها را. برای این موارد از تأمین کنندهی داده (Value Resolver) استفاده کردیم. این تأمین کنندهها میتوانند اینچنین پیاده سازی شوند:
|
1
2
3
4
5
6
7
8
|
public class BookCountValueResolver : ValueResolver<Student, int>{ protected override int ResolveCore(Student source) => source.Books.Count;};public class BookPriceValueResolver : ValueResolver<Student, decimal>{ protected override decimal ResolveCore(Student source) => source.Books.Sum(b => b.Price);}; |
نحوهی پیکربندی و مشاهدهی نتایج را در یک برنامهی تحت کنسول پیاده سازی کردم. متد Main آن میتواند اینچنین باشد:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
static void Main(string[] args){ var assemblyToLoad = Assembly.GetAssembly(typeof (AdminStudentViewModel));//get assembly global::AttributesForAutomapper.Configuration.Initialize(assemblyToLoad);//init automaper IList<Student> lst; using (var context = new MySampleContext()) { lst = context.Students.Include(x => x.Books).ToList(); } foreach (var student in lst) { WriteLine( $"[{student.Id}]*\n{student.Name} {student.Family}.\nmailto:{student.Email}.\nRegistered at'{student.RegisterDateTime}'"); foreach (var book in student.Books) WriteLine($"\tBook name:{book.Name}, Book price:{book.Price}"); } var lstViewModel = AutoMapper.Mapper.Map<IList<Student>, IList<AdminStudentViewModel>>(lst); foreach (var adminStudentViewModel in lstViewModel) { WriteLine( $"[{adminStudentViewModel.Id}]*\n\t{adminStudentViewModel.FirstName} {adminStudentViewModel.LastName}.\n\t" + $"mailto:{adminStudentViewModel.Email}.\n\tRegistered at'{adminStudentViewModel.RegisterDateTimePersian}'\n\t" + $"Book Counts: {adminStudentViewModel.BookCounts} with total price of {adminStudentViewModel.TotalBookPrice}"); } WriteLine("Press any key to exit..."); ReadKey();} |
ابتدا اسمبلی مربوط به ویوومدلها را مشخص میکنیم. سپس این اسمبلی را جهت تبدیل ویژگیها به نگاشتهای معتبر automapper به متد Initialize ارسال میکنیم. تنها بکار بردن همین دوسطر برای اعمال تنظیمها مورد نیاز میباشد. بعد از اجرای موفق متد Initialize، نگاشتهای اشیاء آماده هستند.
نمونهی خروجی:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
[1]*Morteza Raeisi.mailto:MrRaeisi@outlook.com.Registered at'23/08/1392 19:11:43' // I'm using Windows 10 with Persian calendar as default, On other OS or calendar settings, this value is different. Book name:AutoMapper Attr, Book price:1000.00 Book name:Second Book, Book price:2500.00 Book name:Hungry Book, Book price:2500.00...[1]*Morteza Raeisi. //MapForMemebersmailto:. // IgnoreMapRegistered at'1392/08/23 19:11' // Convert usingBook Counts: 3 with total price of 6000.00 // Value resolvers... |
همانطور که میدانید، در MVC برای اعتبارسنجی دادهها در سمت کلاینت از کتابخانهی jquery استفاده میشود. مایکروسافت از طریق jquery.validate.unobtrusive و گسترش کتابخانهی jquery.validate توانسته منطق خود را برای اعتبارسنجی دادهها در سمت کلاینت پیاده سازی کند.
برای این منظور MVC به کنترلهایی که باید اعتبارسنجی شوند، خصوصیاتی را از طریق Data Attribute اضافه میکند. برای مثال اگر در مدل خود فیلد ایمیل را به شکل زیر امضاء کرده باشید:
|
1
2
3
4
5
|
[Display(Name = "رایانامه")][Required(AllowEmptyStrings = false, ErrorMessage = "رایانامه خود را وارد کنید.")][RegularExpression("\\w+([-+.']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*", ErrorMessage = "نشانی رایانامه پذیرفتنی نمیباشد.")][ExistField(Action = "EmailExist", Namespace = "Parsnet.Controllers", Controller = "Account", ErrorMessage = "این رایانامه پیشتر به کار گرفته شده است.")] public string Email { get; set; } |
و در View مورد نظر از Htmlhlper مربوطه به شکل زیر استفاده کرده باشید:
|
1
|
@Html.TextBoxFor(m => m.Email, new { @class = "form-control en", placeholder = @Html.DisplayNameFor(m => m.Email) }) |
در نهایت، Html خروجی در سمت کلاینت به شکل زیر خواهد بود:
|
1
|
<input data-val="true" data-val-existfiledvalidator="این رایانامه پیشتر به کار گرفته شده است." data-val-existfiledvalidator-url="/account/emailexist" data-val-regex="نشانی رایانامه پذیرفتنی نمیباشد." data-val-regex-pattern="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" data-val-required="رایانامه خود را وارد کنید." id="Email" name="Email" placeholder="رایانامه" value="" type="text"> |
و در اینجا کتابخانهی اعتبارسنجی MVC با استفاده از همین خصوصیات *-data، اطلاعات مورد نیاز را جهت نمایش، اعتبارسنجی، تنظیم و بکارگیری، مورد استفاده قرار میدهد.
در یکی از پروژههایی که در حال کار کردن بر روی آن هستم لازم شد تا این اطلاعات اعتبارسنجی به یک تگ span اعمال شوند. سناریوی مورد نظر به این صورت است که در بخش پروفایل کاربر، کاربر میتواند اطلاعات خود را بصورت inline ویرایش کنید. برای اینکار از کتابخانه X-editable استفاده کردم که از این لینک قابل دریافت است.
ابتدا اطلاعات موردنیاز در یک تگ span نمایش داده میشوند و در ادامه کاربر پس از کلیک بر روی آیکن ویرایش، امکان تغییر آن فیلد را دارد. برای اعتبارسنجی دادهها لازم بود تا تمامی اطلاعات مورد نیاز اعتبارسنجی در سمت کلاینت را به شکلی در اختیار داشته باشم و به ذهنم رسید تا با ایجاد یک Helper سفارشی، خصوصیات موردنظر را به تگ span اعمال کنم و سپس در سمت کلاینت از آن استفاده کنم. در واقع با اینکار با استفاده از همان کلاس مدل و این Helper سفارشی، از وارد کردن دستی دادهها و خصوصیات اجتناب کنم. (تصور کنید چیزی حدود 30 فیلد که هرکدام حداقل 4 خصوصیت دارند)
با نگاهی به سورس MVC دیدم پیاده سازی این قابلیت چندان سخت نیست و به راحتی با ایجاد یک Helper سفارشی، منطق خود را پیاده سازی و اعتبارسنجی در سمت کلاینت را به راحتی اعمال کردم.
برای ایجاد این Helper سفارشی ابتدا یک کلاس استاتیک ایجاد کنید و با استفاده از extension Methodها یک helper جدید را ایجاد کنید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
namespace Parsnet{ public static MvcHtmlString SpanFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes) { var sb = new StringBuilder(); var span = new TagBuilder("span"); var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData); var name = ExpressionHelper.GetExpressionText(expression); var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); var value = ""; if (metadata.Model != null && metadata.Model.GetType() == typeof(List<IdentityProvider.IdentityRole>)) { var modelList = (List<IdentityProvider.IdentityRole>)metadata.Model; value = String.Join("، ", modelList.Select(r => r.Name)); } else { value = htmlHelper.FormatValue(metadata.Model, null); } span.MergeAttributes<string, object>(((IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); var fieldName = fullName.Split('.')[1]; span.MergeAttribute("data-name", fieldName, true); span.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); sb.Append(span.ToString(TagRenderMode.StartTag)); sb.Append(value); sb.Append(span.ToString(TagRenderMode.EndTag)); return new MvcHtmlString(sb.ToString()); } }} |
ما در این helper سفارشی از عبارتهای لامبدا استفاده میکنیم و با استفاده از این عبارات، فیلد مورد نظر مدل خود را به helper معرفی میکنیم. آرگومان htmlAttributes در متد helper نیز برای دریافت خصوصیات اضافی helper است؛ خصوصیاتی مانند class، id, style و غیره.
با استفاده از کلاس TagBuilder تگ مورد نظر خود را ایجاد میکنیم. در اینجا من تگ span را ایجاد کردهام که شما میتوانید هر تگ دلخواه دیگری را نیز ایجاد کنید. اولین مرحله، استخراج اطلاعات موردنیاز از metadata مدل است که در خط زیر با پردازش عبارت لامبدا اینکار صورت میگیرد:
|
1
|
var metadata = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData); |
سپس نام فیلد مورد نظر را از مدل استخراج میکنیم:
|
1
2
|
var name = ExpressionHelper.GetExpressionText(expression);var fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name); |
کدهای فوق نام فیلد جاری (در اینجا Email) را از MetaData برای ما استخراج میکند. متغیر value برای نگهداری مقدار این فیلد از مدل است. مرحله بعد استخراج مقدار فیلد و انتساب آن به متغیر value است.
در سناریوی من کاربر میتواند زمینهی فعالیت خود را انتخاب کند که به صورت IdentityRole پیاده سازی شده است. من در اینجا چک میکنیم که اگر نوع دادهای این فیلد List<IdentityProvider.IdentityRole> بود زمینه فعالیت کاربر را از طریق "،" از هم جدا کرده و به صورت یک رشته تبدیل میکنم. در غیر اینصورت همان مقدار عادی فیلد را بکار میگیرم.
|
1
2
3
4
5
6
7
8
9
|
if (metadata.Model != null && metadata.Model.GetType() == typeof(List<IdentityProvider.IdentityRole>)) { var modelList = (List<IdentityProvider.IdentityRole>)metadata.Model; value = String.Join("، ", modelList.Select(r => r.Name)); } else { value = htmlHelper.FormatValue(metadata.Model, null); } |
سپس خصوصیات سفارشی خود را که بصورت attributeهای HTML هستند، در خط زیر به تگ سفارشی اعمال میشوند:
|
1
|
span.MergeAttributes<string, object>(((IDictionary<string, object>)HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); |
مهمترین مرحله که در واقع هدف اصلی من بود، استخراج خصوصیتهای *-data برای اعتبارسجی است که در خط زیر اینکار صورت گرفته است:
|
1
|
span.MergeAttributes<string, object>(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata)); |
نحوهی استفاده از این helper سفارشی هم خیلی ساده است:
|
1
|
@Html.SpanFor(m => m.Profile.Email, new { @class = "editor", data_type = "text" }) |
و در نهایت HTML خروجی به شکل زیر است:
|
1
|
<span class="editor" data-name="Email" data-type="text" data-val="true" data-val-existfiledvalidator="این رایانامه پیشتر به کار گرفته شده است." data-val-existfiledvalidator-url="/account/emailexist" data-val-regex="نشانی رایانامه پذیرفتنی نمیباشد." data-val-regex-pattern="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" data-val-required="رایانامه خود را وارد کنید.">alireza_s_84@yahoo.com</span> |
دیدن شکلهای زیر خالی از لطف نیستند:
و پس از ویرایش:
البته برای درک بهتر این موضوع سعی خواهم کرد تا با یک مثال عملی کامل، نحوهی پیاده سازی را در همینجا قرار دهم.
نگاشت اشیاء امری مفید و لذت بخش است. ولی بخاطر تنظیمات خاص آن و افزایش کدها، همیشه کمی دردسر ساز بوده است. استفاده از کلاس Profile راه کار مناسبی است؛ اما در این حالت کلاس مقصد (ViewModel) از تنظیمات نگاشتها بی اطلاع میماند و فقط حاوی داده خواهد بود. برای ادغام کلاس و تنظیمات نگاشت در اینجا راهکاری ارائه گردید که در ادامه و با الگو گیری از همین ایده، اقدام به ارائهی روشی جدید میکنم که با استفاده از Attributeها تنظیمات نگاشت اشیاء را در AutoMapper انجام میدهد.
در نهایت میخواهیم نگاشتها را اینچنین تنظیم کنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
[MapFrom(typeof (Student), ignoreAllNonExistingProperty: true, alsoCopyMetadata: true)]public class AdminStudentViewModel{ // [IgnoreMap] public int Id { set; get; } [MapForMember("Name")] public string FirstName { set; get; } [MapForMember("Family")] public string LastName { set; get; } public string Email { set; get; } [MapForMember("RegisterDateTime")] public string RegisterDateTimePersian { set; get; } [UseValueResolver(typeof (BookCountValueResolver))] public int BookCounts { set; get; } [UseValueResolver(typeof (BookPriceValueResolver))] public decimal BookPrice { set; get; }}; |
این سبک تنظیم کردن نگاشتهای اشیاء به نظر بهتر از روشهای دیگر است؛ چون کلاسهای ویوومدل را معنادار کرده و همچنین برای برنامه نویسان EF و ASP.NET MVC استفادهی از ویژگیها، یک شیوهی کاری معمول به حساب میآید.
به تعریف و توضیح صفتهای (ویژگیها یا Attributes) مورد نیاز میپردازم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]public class MapFromAttribute : Attribute{ public Type SourceType { get; private set; } public bool IgnoreAllNonExistingProperty { get; private set; } public bool AlsoCopyMetadata { get; private set; } //Go to: http://www.dotnettips.info/courses/topic/16/cb36bc2e-4263-431e-86a5-236322cb5576 public MapFromAttribute(Type sourceType, bool ignoreAllNonExistingProperty = false, bool alsoCopyMetadata = false) { SourceType = sourceType; IgnoreAllNonExistingProperty = ignoreAllNonExistingProperty; AlsoCopyMetadata = alsoCopyMetadata; }}; |
این صفت روی کلاسها مینشیند و توسط آرگومان sourceType آن، نوع مبدأ را برای automapper مشخص میکند. در واقع همه چیز از اینجا شروع میشود. همچنین آرگومان ignoreAllNonExistingProperty مشخص میکند کلیهی صفاتی که در مقصد هستند ولی معادل اسمی در مبدأ ندارند، بصورت خودکار رد (Ignore) شده و از آنها صرف نظر شود تا از شکست متد AutoMapper.Mapper.AssertConfigurationIsValid جلوگیری کند (پرداخته شده در اینجا). آرگومان alsoCopyMetadata پیاده سازی نمیشود؛ ولی میتواند پرچمی باشد تا اجازه دهد Data Annotations از مدلهای ef به ViewModel انتقال یابند.
|
1
2
|
[AttributeUsage(AttributeTargets.Property)]public class IgnoreMapAttribute : Attribute {}; |
از این صفت برای رد کردن خصیصهای در نگاشتها استفاده میکنیم. لازم به ذکر است که صفتی مشابه در Automapper.IgnoreAttribute وجود دارد که میتواند به جای این صفت مورد استفاده قرار گیرد. «نگارنده جهت همخوانی با سایر صفات، اقدام به استفادهی از این صفت میکند»
|
1
2
3
4
5
6
7
8
9
|
[AttributeUsage(AttributeTargets.Property)]public class MapForMemberAttribute : Attribute{ public string MemberToMap { get; private set; } public MapForMemberAttribute(string memberToMap) { MemberToMap = memberToMap; }}; |
اگر نام خصیصهها در مبدأ و مقصد یکی نباشند، از این صفت برای همگام سازی این دو استفاده میکنیم.
|
1
2
3
4
5
6
7
8
9
|
[AttributeUsage(AttributeTargets.Property)]public class UseValueResolverAttribute : Attribute{ public IValueResolver ValueResolver { get; private set; } public UseValueResolverAttribute(Type valueResolver) { ValueResolver = valueResolver.GetConstructors()[0].Invoke(new object[] {}) as IValueResolver; }}; |
استفاده از ValueResolverها در اینجا ذکر شده است. از این صفت برای تنظیم این مقدار برای یک خصیصه استفاده میشود. برای مثال فیلد FullName را در مقصد درنظر بگیرد که از دو فیلد Name و Family در مبدأ تشکیل میشود.
تا اینجا صفات پیش نیاز کار فراهم شدند. حال باید این صفتها را به نگاشت متناسبی در automapper تبدیل کنیم.
در مقالهی قبلی ( + ) به این لحاظ که بهترین راه نشان دادن نحوهی کارکرد Controller Factory ایجاد یک نمونهی سفارشی بود، آن رابررسی کردیم و برای اکثریت برنامهها و سناریوها، کلاس توکار Controller Factory به نام DefaultControllerFactory کفایت میکند.
پس از وصول یک درخواست از طریق سیستم مسیریابی، factory پیش فرض (DefaultControllerFactory) به بررسی rout data پرداخته تا خاصیت Controller آن را بیابد و سعی در پیدا کردن کلاسی در برنامه خواهد داشت که مشخصات ذیل را دارا باشد:
- دارای سطح دسترسی public باشد.
- Abstract نباشد.
- حاوی پارامتر generic نباشد.
- نام کلاس دارای پسوند Controller باشد.
- پیاده سازی کننده اینترفیس IContoller باشد.
کلاس DefaultControllerFactory در صورت یافتن کلاسی مطابق قواعد فوق و مناسب درخواست رسیده، وهلهای از آن را به کمک Controller Activator ایجاد میکند. میبینید که با برپایی چند قاعدهی ساده، factory پیش فرض، نیاز به ثبت کنترلرها را به منظور معرفی و داشتن لیستی برای بررسی از طرف برنامه نویس (مثلا درج نام کلاسهای کنترلر در یک فایل پیکربندی)، مرتفع ساخته است.
اگر بخواهید به فضاهای نام خاصی برای یافتن آنها توسط factory پیش فرض، برتری قائل شوید، باید در متد Application_Start فایل global.asax.cs مانند ذیل عمل نمایید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Http;using System.Web.Mvc;using System.Web.Routing;using ControllerExtensibility.Infrastructure;namespace ControllerExtensibility{ public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace"); ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*"); } }} |
مهمترین دلیلی که نیاز داریم factory پیش فرض را سفارشی کنیم، استفاده از تزریق وابستگیها (DI) به کنترلرهاست. راههای متعددی برای این کار وجود دارند که انتخاب بهترین روش بسته به چگونگی بکارگیری DI در برنامه شماست:
کدهای اینترفیس IControllerActivator مطابق ذیل است:
|
1
2
3
4
5
6
7
8
|
namespace System.Web.Mvc{ using System.Web.Routing; public interface IControllerActivator { IController Create(RequestContext requestContext, Type controllerType); }} |
این اینترفیس حاوی متدی به نام Create است که شیء RequestContext به آن پاس داده میشود و یک Type که مشخص میکند کدام کنترلر باید وهله سازی شود. در کدهای ذیل در قسمت (return (IController)ObjectFactory.GetInstance(controllerType فرض بر این است که در پروژه برای تزریق وابستگی، StructureMapFactory را به کار گرفتهایم و سیم کشیهای لازم قبلا صورت گرفته است. چنانچه با StructureMap آشنایی ندارید به این مقاله سایت (استفاده از StructureMap به عنوان یک IoC Container) مراجعه نمایید.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
using ControllerExtensibility.Controllers;using System;using System.Web.Mvc;using System.Web.Routing;namespace ControllerExtensibility.Infrastructure{ public class StructureMapControllerActivator : IControllerActivator { public IController Create(RequestContext requestContext, Type controllerType) { return (IController)ObjectFactory.GetInstance(controllerType); } }} |
برای استفاده از این activator سفارشی نیاز داریم وهلهای از آن را به عنوان پارامتر به سازندهی کلاس DefaultControllerFactory ارسال کنیم و نتیجه را در متد Application_Start فایل global.asax.cs ثبت کنیم.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Http;using System.Web.Mvc;using System.Web.Routing;using ControllerExtensibility.Infrastructure;namespace ControllerExtensibility{ public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new StructureMapControllerActivator())); } }} |
میتوان متدهای کلاس مشتق شدهی از DefaultControllerFactory را override کرد و برای اهدافی نظیر DI از آن بهره جست. جدول ذیل سه متدی که میتوان با تحریف آنها به مقصود رسید، توصیف شدهاند:
| متد | نوع بازگشتی | توضیحات |
| پیاده سازی کنندهی متد Createontroller از اینترفیس IControllerFactory است و به صورت پیش فرض متد GetControllerType را جهت تعیین نوعی که باید وهله سازی شود، صدا میزند و سپس کنترلر وهله سازی شده را به متد GetControllerInstance ارسال میکند. | ||
| وظیفهی نگاشت درخواست رسیده را به Controller type عهده دار است. | ||
| وظیفه ایجاد وهلهای از نوع مشخص شده را عهده دار است. |
شیوهی تحریف متد GetControllerInstance
|
1
2
3
4
5
6
7
|
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } |
شیوهی ثبت در فایل global.asax.cs و در متد Application_start :
|
1
|
ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); |
یکی از مزایای مهم فریم ورک ASP.NET MVC، توسعه پذیری کنترلرهای آن است. با مرور قسمتهایی از مسیر پردازش درخواست که منجر به اجرای یک اکشن متد میشود، شروع میکنیم و روشهای مختلفی را که میتوان بر روی این پردازش، کنترل داشت، بررسی میکنیم. شکل ذیل مسیر یک درخواست را مابین کامپوننتهای مختلف فریم ورک نشان میدهد:
Controller Factory و Action Invoker وظیفهای مطابق نامشان را عهده دار هستند. اولی برای وهله سازی کنترلرهای مرتبط با درخواست و دومی برای پیدا کردن و تریگر نمودن یک اکشن متد به کار گرفته میشوند. فریم ورک MVC پیاده سازی پیش فرضی را از این دو کامپوننت، به صورت توکار دارد. در طی مقالاتی نحوهی کنترل کردن رفتار پیش فرض این Controller Factory و هم نحوهی جایگزین کرن کامل این کامپوننت را بررسی میکنیم.
ابتدا پروژهی جدیدی را از نوع MVC و با الگوی Empty به نام ControllerExtensibility ایجاد میکنیم. در پوشهی Models یک فایل را به نام Result.cs ساخته و از آن برای معرفی کلاس Result مطابق کدهای ذیل استفاده میکنیم:
|
1
2
3
4
5
6
7
8
|
namespace ControllerExtensibility.Models{ public class Result { public string ControllerName { get; set; } public string ActionName { get; set; } }} |
در مسیر /Views/Shared ویویی را به نام Result.cshtml اضافه میکنیم. این ویویی است که در این مثال، همهی اکشن متدهای کنترلرهایمان، آن را رندر خواهند کرد:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@model ControllerExtensibility.Models.Result@{Layout = null;}<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width" /><title>Result</title></head><body><div>Controller: @Model.ControllerName</div><div>Action: @Model.ActionName</div></body></html> |
در خط اول، مدل ویو را از نوع کلاس Result تعیین کردهایم.
دو کنترلر را نیز حاوی کدهای زیر ایجاد میکنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
using ControllerExtensibility.Models;using System.Web.Mvc;namespace ControllerExtensibility.Controllers{ public class ProductController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Product", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Product", ActionName = "List" }); } }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using System.Web.Mvc;namespace ControllerExtensibility.Controllers{ public class CustomerController : Controller { public ViewResult Index() { return View("Result", new Result { ControllerName = "Customer", ActionName = "Index" }); } public ViewResult List() { return View("Result", new Result { ControllerName = "Customer", ActionName = "List" }); } }} |
اکشنهای این دو کنترلر حاوی کد خاصی نبوده و صرفا ویوی Result.cshtml را صدا میزنند. ولی در این مرحله این همهی آن چیزی است که برای نشان دادن نحوهی سفارشی کردن کنترلرها بدان نیاز داریم.
ایجاد یک Controller Factory سفارشی بهترین راه برای درک نحوهی وهله سازی کنترلرها توسط MVC است. ولی این کار صرفا جنبهی آموزشی داشته و در یک پروژهی واقعی این نوع پیاده سازیها پیشنهاد نمیشود؛ زیرا راههای مفیدتر و سادهتری با پیاده سازی توکار Controller Factory وجود دارند.
Controller Factoryها با پیاده سازی اینترفیس IControllerFactory معرفی میشوند. کدهای این اینترفیس را در ذیل میبینید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using System.Web.Routing;using System.Web.SessionState;namespace System.Web.Mvc{ public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); }} |
پوشهای را به نام Infrastructure ساخته و فایلی را به نام CustomControllerFactory.cs ، حاوی کدهای زیر اضافه کنید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
using System;using System.Web.Mvc;using System.Web.Routing;using System.Web.SessionState;using ControllerExtensibility.Controllers;namespace ControllerExtensibility.Infrastructure{ public class CustomControllerFactory : IControllerFactory { public IController CreateController(RequestContext requestContext, string controllerName) { Type targetType = null; switch (controllerName) { case "Product": targetType = typeof (ProductController); break; case "Customer": targetType = typeof (CustomerController); break; default: requestContext.RouteData.Values["controller"] = "Product"; targetType = typeof (ProductController); break; } return targetType == null ? null : (IController) DependencyResolver.Current.GetService(targetType); } public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) { return SessionStateBehavior.Default; } public void ReleaseController(IController controller) { IDisposable disposable = controller as IDisposable; if (disposable != null) { disposable.Dispose(); } } }} |
مهمترین متد کدهای فوق، CreateController است که فریم ورک، بر حسب نیاز، جهت سرویس دهی به درخواست واصله آن را صدا خواهد زد. پارامتر ورودی این متد، شیء RequestContext است که جزئیاتی در خصوص درخواست واصله را در اختیار factory خواهد گذاشت. همچنین یک رشته که نام کنترلر را بر حسب URL واصله تعیین میکند:
|
نام |
نوع |
توضیحات |
|
|
|
حاوی اطلاعاتی در خصوص درخواست است. |
|
|
|
حاوی اطلاعاتی در خصوص Rout است که با درخواست رسیده همخوانی دارد. |
یکی از دلایلی که عنوان شد Controller factory سفارشی بدین روش در یک پروژهی عملی به کار گرفته نشود این است که یافتن کلاسهایی از نوع Controller در سراسر برنامه و وهله سازی آنها کار دشواری است. چرا که لازم خواهد بود بتوانید به صورت پویا کنترلر را مکان یابی کرده و بین کلاسهای هم نام در دیگر فضاهای نام تمییز قائل شوید و خطاهای محتمل در حین وهله سازی را کنترل کنید.
در این مثال تنها دو کنترلر داریم و آنها را به صورت مستقیم در Controller Factory وهله سازی میکنیم که در یک پروژهی واقعی مطلوب نیست. ولی آنچه را که این روش آشکارتر میسازد، انعطاف پذیری بالای فریم ورک MVC است که دست ما را برای نفوذ و دخل و تصرف در اعمال و رفتاریهای پیش فرض خود باز گذاشته است و برای مثال در مباحث تزریق وابستگیها و تنظیمات ابتدایی IoC Containers کاربرد دارد.
متد CreateController لازم است وهلهای از کلاسی که اینترفیس IController را پیاده سازی کرده برگرداند؛ در غیر اینصورت کار با خطا متوقف خواهد شد. لذا برای زمانی که درخواست کاربر، هیچ کدام از کنترلرها را مشمول عنایت قرار نمیدهد، باید چارهای اندیشیده شود.
میتوان آن را به کنترلر خاصی که پیغام خطایی را رندر میکند، هدایت کنیم. به عبارت بهتر باید درخواست را به کنترلری که مطمئن هستیم وجود دارد (اصطلاحا کنترلر جانشین) هدایت نماییم. همان طور که در کد فوق در قسمت default میبینید:
|
1
2
3
4
|
default:requestContext.RouteData.Values["controller"] = "Product";targetType = typeof(ProductController);break; |
در صورت عدم تطابق با هیچ کدام از حالات تعیین شده، درخواست را به کنترلر ProductController جهت رسیدگی هدایت کردهایم.
در MVC انتخاب ویوی مناسب، بر حسب مقدار RouteData.Values صورت میگیرد؛ نه نام کلاس Controller و این سبب خواهد شد فریم ورک، ویوهای مرتبط با کنترلر جانشین شدهی توسط ما را جستجو کند و نه کنترلری که کاربر از طریق URL ورودی آن را درخواست کرده است.
لذا Controller Factory صرفا وظیفه مپ کردن درخواستهای واصله به کنترلرها را ندارد، بلکه توانایی دخل و تصرف در درخواست واصله بر حسب مورد را نیز خواهد داشت.
در نهایت هم نحوهی استفاده از DependencyResolver را برای وهله سازی کلاسهای کنترلر میبینید. متد استاتیک Current یک پیاده سازی از اینترفیس IDependencyResolver را که حاوی متد GetService است، برگشت داده و سپس یک شیء System.Type را به عنوان ورودی گرفته و یک وهلهی ساخته شدهی از آن را به عنوان خروجی برمیگرداند.
متد GetControllerSessionBehavior نیز توسط MVC جهت تعیین اینکه Session data برای کنترلر نیاز است یا خیر به کار گرفته میشود.
متد ReleaseController نیز هر گاه به شیء کنترلر ساخته شده در متد CreateController دیگر نیازی نبود، صدا زده خواهد شد. در کدهای ما ابتدا بررسی میشود آیا اینترفیس IDisposable توسط کلاس، پیاده سازی شده است یا خیر؟ اگر بلی متد Dispose آن جهت آزاد سازی منابعی که میتوانند آزاد شوند، صدا زده میشود.
جهت ثبت Controller Factory ساخته شده در متد Application_Start موجود در فایل global.asax.cs بوسیله کلاس ControllerBuilder و مطابق کدهای ذیل عمل مینماییم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Http;using System.Web.Mvc;using System.Web.Routing;using ControllerExtensibility.Infrastructure;namespace ControllerExtensibility{ public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory()); } }} |
پس از ثبت به شیوهی فوق، controller factory ساخته شده، مسئول هندل کردن تمامی درخواستهای واصلهی به برنامه خواهد بود. پس از اولین اجرا، مرورگر ریشهی سایت را هدف قرار خواهد داد که توسط سیستم مسیر یابی به کنترلر Home، نگاشت شده و بر اساس تعاریف و کدهای ما، چون با هیچ کدام از کنترلرهای Product و Customer تطابق نخواهد داشت، به کنترلر جایگزین تنظیم شده، یعنی Product هدایت خواهد شد.
در asp.net تعدادی اشیاء پایه، حاوی اطلاعات بسیار با ارزشی در خصوص درخواست جاری، application و پاسخی که ارسال میشود هستند و به صورت غیر مستقیم جهت دستیابی به قسمتهای مرکزی و هستهای چهارچوب asp.net مانند security , stat data میتوان این اشیاء را بکار گرفت.
بررسی این اشیاء از این جهت حائز اهمیت است که در کنترلرها و ویوها میتوان پاسخهای ارسالی به کلاینتها را بر حسب شرایط مختلفی مانند درخواست رسیده یا حالت خاص دیگری تغییر داد. ضمن اینکه از این اشیاء در ماژولها و هندلرها نیز استفاده میشود. لذا قبل از پرداختن به مفاهیم مرتبط به ماژولها و هندلرها بهتر است این اشیاء بررسی شوند.
کلاس httpcontext هستهی چهارچوب Asp.net است که خواص بسیار مهمی به شرح ذیل را فراهم میآورد:
: شیء HttpAplicationState را جهت مدیریت Application State Data بر میگرداند.
: شیء HttpApplication مرتبط با درخواست جاری را بر میگرداند.
: شیء Cache را جهت کش دادهها بر میگرداند.
: خصوصیت استاتیکی که شیء HttpContext درخواست جاری را بر میگرداند.
: وهلهای از IHttpHandler را که محتویات درخواست جاری را تولید خواهد کرد، بر میگرداند.
: مقدار True یا False را بسته به اینکه Debug فعال باشد یا خیر بر میگرداند.
: یک Collection را که میتواند جهت انتقال State Data بین اجزاء مختلف فریم ورک که در پردازش یک درخواست همکاری میکنند، بر گرداند.
: قسمت خاصی از فایل Web.Config را بر میگرداند.
: شیء HttpRequest را که حاوی جزئیات درخواست پردازش شده است، بر میگرداند.
: شیء HttpResponse را بر میگرداند که حاوی جزئیات Response ای است که آماده ارسال به مرورگر است.
: برگردانندهی شیء HttpSession است. این خاصیت قبل از وقوع رخداد PostAcquireRequestState مربوط به Application، مقدار null را خواهد داشت.
: شیء از نوع Datetime و معرف زمانی است که شیء HttpContext ایجاد شدهاست.
: جهت ثبت اطلاعات تشخیصی به کار میرود.
در فایل global از طریق خاصیت Context به وهلهای از HttpContext دسترسی داریم. ولی این خاصیت فراگیر نبوده و در ویوها و کنترلرها از طریق خاصیت HttpContext در دسترس است که در کنترلر خاصیتی از کلاس پایه Controller و در ویو خاصیتی از کلاس پایه WebViewPage میباشد و در ماژولها از طریق پارامتر ورودی متد Init در دسترس است.
در هندلرها متد ProcessRequest وهلهای از شیء HttpContext را دریافت میکند.
به طور سراسری در هر نقطهای از برنامه از طریق خاصیت استاتیک HttpContext.Current به شیء HttpContext مرتبط با درخواست جاری دسترسی خواهیم داشت.
در کلاسهای مدل نیز از طریق خاصیت استاتیک یاد شده میتوان به این شیء دسترسی داشت؛ ولی بهتر است اگر مدل نیاز به اطلاعاتی در خصوص یک Request داشت این اطلاعات از طریق اشیاء Context در کنترلر دریافت و به عنوان آرگومان به مدل ارسال شوند.
به صورت کلی، خلاصهی مطالب فوق به صورت ذیل میباشد:
| بخش مورد نظر | روش دسترسی به اشیاء context |
| از طریق خاصیت HttpContext | |
| خاصیت Context | |
| خاصیت Context | |
| آرگومان متد Init | |
| آرگومان متد ProcessRequest | |
| از طریق خاصیت استاتیک HttpContext.Current |
خواص ذکر شدهی در جدول فوق، همگی نوع یکسانی ندارند. در فایل global و هندلرها و ماژولها، دقیقا به همان HtpContext ای دسترسی داریم که قبلا با آنها آشنا شدیم. این نوع از اشیاء (منظور خواص کلاس HttpContext) قبل از اینکه فریم ورک MVC، دست به کار مدیریت یک درخواست شود، کار خود را شروع میکنند که این امر کار آزمایش واحد را مشکل میکند. برای رفع این مشکل خواص مهیا در کنترلر و ویو وهلههایی از کلاسهای متفاوتی را برمی گردانند که از کلاس Context مشتق شدهاند و ضمنا آزمایش واحد را به سادگی پشتیبانی میکنند(+ ) . خاصیت HttpContext کلاس Controller وهلهای از کلاس HttpContextBase را بر میگردانند. همهی اشیاء context موجود در این کلاس، دارای پسوند Base هستند؛ مانند : HttpRequestBase, HttpResponseBase و...
اگر نیاز داشتید که یک شیء از نوع پسوند دار Base را از یک شیء فاقد این پسوند ایجاد کنید، برای مثال یک شیء HttpRequest دارید، ولی متدی دارید که آرگومان آن وهلهای از کلاس HttpRequestBase است، برای این کار کلاسهایی با پسوند Wrapper مانند HttpContextWrapper ,… وجود دارند که از کلاسهایی با پسوند Base مشتق شده و در متد سازنده خود آرگومانی از کلاسهای اصلی فاقد پسوند Base را میگیرند.
ولی عکس آن امکان پذیر نیست و نمیتوان کلاس با پسوند Base را به نوع اولیه برگرداند. ولی همان طور که گفتیم هر جایی که نیاز داشتیم، میتوان از طریق خاصیت استاتیک System.Web.HTTPContext.Current به اشیاء Context اصلی دسترسی داشت.
بسیاری از کلاسهای فریم ورک Asp.Net دارای خاصیتهایی هستند که به خوااص کلاس HttpContext نگاشت شدهاند. یکی از این نمونهها کلاس HttpApplication است که کلاس پایهی فایل global نیز هست. همان طور که در ذیل نیز میبینید، بسیاری از خواص و متدهای کلاس HttpApplication به نوعی مرتبط با اشیاء context هستند.
: به خاصیت HttpContext.Application نگاشت شدهاست که دسترسی به state data application را مهیا میسازد.
: به چرخه حیات جاری خاتمه خاتمه داده و آن را مستقیما به رخداد LogRequest منتقل میکند.
: شیء HttpContext مرتبط با درخواست جاری را بر میگرداند.
: هر گاه متد Init یکی از ماژولهای ثبت شده در برنامه فراخوانی شود، این متد صدا زده خواهد شد.
: یک شیء HttpModuleCollection را که حاوی جزئیات ماژولهای برنامه است، بر میگرداند.
: متد استاتیکی است که یک ماژول جدید را ثبت میکند.
: مقدار HttpContext.Request را برگشت میدهد و در صورتی که این مقدار null باشد، استثنایی رخ خواهد داد.
: مقدار HttpContext.Response را برگشت داده و در صورت null بودن این مقدار، استثنایی رخ میدهد.
: به خاصیت HttpContext.Server نگاشت شده است.
: مقدار HttpContext.Session را برگشت میدهد که در صورت null بودن این مقدار، استثنایی رخ خواهد داد.
در این مقاله با خواص کلاس HttpContext که اشیائی مهم و پرکاربرد بوده و حاوی اطلاعات بسیار سودمندی هستند، آشنا شدیم. ضمنا همان طور که در ابتدای مقاله گفته شد، از این اشیاء در ماژولها و هندلرها استفادهی زیادی میشود.
بعد از استفاده از گریدهای Grid.mvc , JQGrid, Kendo و مشکلاتی که با هر کدام از آنها داشتم، در نهایت به WebGrid که به صورت توکار وجود دارد، برای استفاده جهت نمایش اطلاعات رسیدم؛ از این جهت که به کتابخانهی جانبی نیازی ندارد و از نظر سرعت و لود شدن بهینه میباشد، البته با اضافه کردن یکسری کدهای css.
برای آشنایی بیشتر با این helper توصیه میکنم ابتدا این مقاله را مطالعه نمایید.
به صورت پیش فرش WbebGrid صفحه بندی را به صورت خیلی ساده فقط با نمایش اعداد و جهت نماهای جلو و عقب، نشان میدهد که برای پروژههای رسمی تا حدودی جالب نیست.
در این مطلب قصد داریم از کتابخانهی bootstrap جهت صفحه بندی استفاده کنیم و در نهایت به صفحه بندی زیر برسیم:
برای اینکار، ابتدا قبل از هر چیزی به یک متد الحاقی برای انجام صفحه بندی سفارشی سازی شده، نیاز داریم که کدهای این متد به صورت زیر خواهد بود:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
public static class WebGridExtensions{ public static HelperResult PagerList( this WebGrid webGrid, WebGridPagerModes mode = WebGridPagerModes.NextPrevious | WebGridPagerModes.Numeric, string firstText = null, string previousText = null, string nextText = null, string lastText = null, int numericLinksCount = 5) { return PagerList(webGrid, mode, firstText, previousText, nextText, lastText, numericLinksCount, explicitlyCalled: true); } private static HelperResult PagerList( WebGrid webGrid, WebGridPagerModes mode, string firstText, string previousText, string nextText, string lastText, int numericLinksCount, bool explicitlyCalled) { int currentPage = webGrid.PageIndex; int totalPages = webGrid.PageCount; int lastPage = totalPages - 1; var ul = new TagBuilder("ul"); var li = new List<TagBuilder>(); if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) { if (String.IsNullOrEmpty(firstText)) { firstText = "اولین"; } var part = new TagBuilder("li") { InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(0), firstText) }; if (currentPage == 0) { part.MergeAttribute("class", "disabled"); } li.Add(part); } if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) { if (String.IsNullOrEmpty(previousText)) { previousText = "قبلی"; } int page = currentPage == 0 ? 0: currentPage - 1; var part = new TagBuilder("li") { InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), previousText) }; if (currentPage == 0) { part.MergeAttribute("class", "disabled"); } li.Add(part); } if (ModeEnabled(mode, WebGridPagerModes.Numeric) && (totalPages > 1)) { int last = currentPage + (numericLinksCount / 2); int first = last - numericLinksCount + 1; if (last > lastPage) { first -= last - lastPage; last = lastPage; } if (first < 0) { last = Math.Min(last + (0 - first), lastPage); first = 0; } for (int i = first; i <= last; i++) { var pageText = (i + 1).ToString(CultureInfo.InvariantCulture); var part = new TagBuilder("li") { InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(i), pageText) }; if (i == currentPage) { part.MergeAttribute("class", "active"); } li.Add(part); } } if (ModeEnabled(mode, WebGridPagerModes.NextPrevious)) { if (String.IsNullOrEmpty(nextText)) { nextText = "بعدی"; } int page = currentPage == lastPage ? lastPage: currentPage + 1; var part = new TagBuilder("li") { InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(page), nextText) }; if (currentPage == lastPage) { part.MergeAttribute("class", "disabled"); } li.Add(part); } if (ModeEnabled(mode, WebGridPagerModes.FirstLast)) { if (String.IsNullOrEmpty(lastText)) { lastText = "آخرین"; } var part = new TagBuilder("li") { InnerHtml = GridLink(webGrid, webGrid.GetPageUrl(lastPage), lastText) }; if (currentPage == lastPage) { part.MergeAttribute("class", "disabled"); } li.Add(part); } ul.InnerHtml = string.Join("", li); var html = ""; if (explicitlyCalled && webGrid.IsAjaxEnabled) { var span = new TagBuilder("span"); span.MergeAttribute("data-swhgajax", "true"); span.MergeAttribute("data-swhgcontainer", webGrid.AjaxUpdateContainerId); span.MergeAttribute("data-swhgcallback", webGrid.AjaxUpdateCallback); span.InnerHtml = ul.ToString(); html = span.ToString(); } else { html = ul.ToString(); } return new HelperResult(writer => { writer.Write(html); }); } private static String GridLink(WebGrid webGrid, string url, string text) { TagBuilder builder = new TagBuilder("a"); builder.SetInnerText(text); builder.MergeAttribute("href", url); if (webGrid.IsAjaxEnabled) { builder.MergeAttribute("data-swhglnk", "true"); } return builder.ToString(TagRenderMode.Normal); } private static bool ModeEnabled(WebGridPagerModes mode, WebGridPagerModes modeCheck) { return (mode & modeCheck) == modeCheck; }} |
کلاس فوق باید در پوشهی App_Code قرار گیرد.
پس از آن در View یی که اطلاعات را نمایش میدهید، فقط لازم است کد زیر را اضافه نمایید:
|
1
2
3
|
<div>@grid.PagerList(mode: WebGridPagerModes.All)</div> |
تا اینجا متد مورد نظر برای انجام صفحه بندی گرید پیاده سازی شد. ادامهی کار هم مشخص است؛ داشتن یک PartialView جهت نمایش لیست اطلاعات، پاس دادن دیتا به Partial و تمام.
در ادامه برای تکمیل بحث مثالی را از نحوهی نمایش اطلاعات و صفحه بندی سفارشی نشان خواهیم داد:
تنظیمات لازم گرید :
|
1
2
3
4
5
6
7
|
@{ WebGrid grid = new WebGrid(Model, rowsPerPage: 10, ajaxUpdateContainerId: "grid"); var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);} |
تعیین فیلدهای گرید :
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@grid.Table( tableStyle: "table table-striped table-hover", headerStyle: "webgrid-header", alternatingRowStyle: "webgrid-alternating-row", selectedRowStyle: "webgrid-selected-row", rowStyle: "webgrid-row-style", columns: grid.Columns( grid.Column(columnName: "Name", header: "نام استان", style: "myfont"), grid.Column(columnName: "NameEn", header: "نام استان ( انگلیسی )", style: "myfont"), grid.Column(header: "", format: item => @Html.ActionLink("مدیریت شهرها", actionName: MVC.Admin.City.ActionNames.Index, controllerName: MVC.Admin.City.Name, routeValues: new {Code=item.Code },htmlAttributes:null)), grid.Column(header: "", style: "text-align-center-col smallcell", format: item => @Html.ActionLink(linkText: "ویرایش", actionName: "Edit", controllerName: "Province", routeValues: new { area = "Admin", code = item.Code }, htmlAttributes: new { @class = "btn-sm btn-info vertical-center" })) ) )<div> @grid.PagerList(mode: WebGridPagerModes.All)</div> |
خروجی حاصل به صورت زیر خواهد بود :
اگر طبق توضیحات بالا عمل کرده باشید، در نهایت صفحه بندی شما به صورت عمودی نمایش داده میشود؛ یعنی هر کدام از شماره صفحات در یک سطر. دلیل آن هم این است که تگ ul، کلاس .pagination را ندارد. در کدهای بوت استراپ تعریف شده است که تمام li هایی که به صورت مستقیم داخل کلاس .pagination هستند خصوصیات مورد نظر را بگیرند.
برای این کار دو راه حل وجود دارد :
راه حل اول: تغییر کدهای css
کدهای نوشته شده برای صفحه بندی در بوت استراپ را از حالت زیر:
|
1
|
.pagination > li |
به حالت زیر تغییر دهید:
|
1
|
.pagination li |
یادآوری : علامت < در CSS یعنی به صورت مستقیم و در شاخهی اول.
راه حل دوم - افزودن کلاس .pagination به تگ ul:
ابتدا کلاس .pagination را از تگ div حذف نمایید:
|
1
2
3
|
<div > @grid.PagerList(mode: WebGridPagerModes.All)</div> |
و در کدهایی کلاس WebGridExtensions، در قسمتی که تگ ul اصافه میشود، کلاس مورد نظر را به آن اضافه میکنیم:
|
1
2
|
var ul = new TagBuilder("ul"); ul.AddCssClass("pagination"); |
دانلود کدهای این مثال
اگر نیاز داشتید به یکباره تمام اطلاعات را در گرید لود نکنید و به صورت n تاn تا رکوردها را نمایش دهید، در این حالت پس از پاس دادن لیستی از اطلاعات به View مورد نظر لازم است تعداد کل رکوردها را در یک متغییر به سمت View بفرستید. این کار به این دلیل میباشد که بتوان صفحه بندی را تولید کرد. برای این کار در بخش تنظیمات Webgrid مقدار source را برابر null قرار دهید و از قطعه کد زیر جهت بایند کردن گرید، بعد از کدهای تنظیمات WebGrid استفاده نمایید:
|
1
|
grid.Bind(Model, rowCount: (int)ViewBag.PageCount); |
در Asp.net دو چرخهی حیات مهم وجود دارند که اساس چارچوب MVC را تشکیل میدهند :
- چرخهی حیات برنامه (Application)؛ از لحظهای که برنامه برای اولین بار اجرا میشود و تا لحظهی خاتمهی آن را شامل میشود.
- چرخهی حیات یک درخواست ( Request )؛ مسیری که یک درخواست طی میکند، اصطلاحا PipeLine نامیده میشود که همان چرخهی حیات یک درخواست نیز هست و از لحظهای که درخواست تحویل asp.net شده، تا زمانیکه درخواست ارسال میشود را شامل میشود.
تمرکز بنده بیشتر بر روی روند و مسیری است که یک درخواست طی میکند و قصد دارم با بهره گیری از کتاب Pro Asp.net Mvc 5 و دیگر منابع، چرخهی حیات درخواست را در برنامههای Mvc بررسی کرده و در مقالات آتی ماژولها و هندلرها را بررسی کنم.
در asp.net ، برنامه global فایلهای شامل دو فایل Global.asax , Global.asax.cs است.
فایل Global.asax که هیچ گاه نیاز به ویرایش آن نداریم محتویاتی مانند زیر دارد:
|
1
|
<%@ Application Codebehind="Global.asax.cs" Inherits="YourAppName.MvcApplication" Language="C#" %> |
و صرفا فایل code behind مرتبط را برای asp.net مشخص میکند. این نوع مشخص سازی را از وب فرمها به یاد داریم.
در این مقاله منظور از فایل global فایل Global.asax.cs است که مشتق شده از کلاس System.Web.HttpApplication است:
|
1
2
3
4
5
6
7
|
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ...// } } |
به صورت پیش فرض کدهای بالا ایجاد شده و کلاس MvcApplication شامل متد Application_Start است که نقطهی شروع چرخهی حیات برنامه را مشخص میکند. اما متد دیگری به نام Application_End() نیز وج ود دارد که در زمان خاتمهی برنامه فراخوانی خواهد شد و فرصتی را برای آزاد سازی منابع اشغال شده توسط برنامه فراهم میآورد .
Asp.net برای پاسخگویی به درخواستهای واصله، وهلههایی از کلاس MvcApplication را میسازد ولی این دو متد صرفا در نقاط شروع و پایان برنامه فراخوانی شده و عملا در وهلههای یاد شده صدا زده نخواهند شد و به جای آنها رویدادهایی را که در ذیل آنها را معرفی میکنیم، فراخوانی شده و چرخهی حیات درخواست را برای ما مشخص میسازند .
: به عنوان اولین رویداد، به محض وصول یک درخواست جدید رخ خواهد داد.
: رویداد AuthenticateRequest برای شناسایی کاربر ارسال کننده درخواست، کاربرد دارد و پس از پردازش کلیهی توابع، رویداد PostAuthenticateRequest صدا زده میشود.
:بههنگام صدور مجوزهای یک درخواست رخ میدهد و مشابه رویداد بالا پس از پردازش کلیهی توابع، رویداد PostAuthorizeRequest صدا زده خواهد شد.
: پس از صدور مجوزهای یک درخواست در رویداد authorization زمانیکه ماژولهای کش میخواهند اطلاعاتی را از کش سرور مطالبه کنند، رخ میدهد و به مانند دو رخداد قبلی، PostResolveRequestCache نیز پس از اتمام پردازش توابع رویداد رخ میدهد.
: زمانی که Asp.net میخواهد هندلری را برای پاسخگویی به درخواست واصله انتخاب کند رخ میدهد و PostMapRequestHandler نیز پس از این انتخاب، تریگر میشود.
: جهت بدست آوردن دادههایی نظیر سشن و ... مرتبط با درخواست جاری کاربرد داشته و PostAcquireRequestState نیز پس از پردازش توابع رویداد رخ خواهد داد.
: بالافاصله قبل و همچنین بلافاصله بعد از این که یک هندلر بخواهد درخواستی را پردازش کند، رخ میدهد. PostRequestHandlerExecute نیز همانند دیگر رویدادهای گذشته، پس از اتمام پردازش توابع، این رویداد رخ خواهد داد.
: زمانی رخ میدهد که دادههای مرتبط با درخواست جاری، در ادامهی روند پردازش درخواست مورد نیاز نباشند و پس از پردازش توابع رویداد، PostReleaseRequestState رخ خواهد داد .
: به جهت اینکه ماژولهای مسئول کش، توانایی به روز رسانی دادههای خود، برای پاسخگویی به درخواستهای بعدی را داشته باشند، این رویداد رخ میدهد.
: قبل از انجام عملیات لاگین برای درخواست جاری رخ میدهد و پس از پردازش توابع رویداد نیز PostLogRequest تریگر میشود.
: پس از پایان کار پردازش درخواست جاری و مهیا شدن پاسخ مرتبط جهت ارسال به مرورگر تریگر خواهد شد.
: قبل از ارسال HTTP headers به مرورگر این رویداد رخ خواهد داد.
: بعد از ارسال شدن هدرها و قبل از ارسال محتوای صفحه به مرورگر رخ میدهد.
: هر زمان و در هر مرحله از پردازش درخواست، چنانچه خطایی صورت پذیرد این رویداد رخ خواهد داد.
فریم ورک Asp.net جهت مدیریت بهتر یک درخواست، در تمام مسیر پردازش، رویدادهای بالا را مهیا کرده است. در ادامه نحوهی هندل کردن رویدادهای چرخهی حیات درخواست را در فایل global توضیح میدهم. هر چند که استفاده از این فایل بدین منظور، صرفا برای مدیریت مسائل ابتدایی مناسب بوده و در یک پروژهی بزرگ موجب به هم ریختگی فایل global با کدهای زیاد و خوانایی پایین بوده که قابلیت استفاده مجدد در دیگر پروژهها را نیز ندارد.
Asp.net این مشکل را با معرفی ماژولها که در مقالات آتی توضیح خواهم داد، مرتفع کرده است.
در فایل global هر گاه متدی را با پیشوند Application_ و نام یکی از رویدادهای بالا بنویسید Asp.net آن را به عنوان هندلری برای رویداد مذکور میشناسد. به عنوان مثال متدی با نام Application_BeginRequest متد رویداد BeginRequest میباشد.
ابتدا یک پروژهی MVC جدید را به نام SimpleApp ایجاد کرده و فایل global آن را مطابق ذیل تغییر میدهیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;using System.Web.Routing;namespace SimpleApp{ public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } protected void Application_BeginRequest() { RecordEvent("BeginRequest"); } protected void Application_AuthenticateRequest() { RecordEvent("AuthenticateRequest"); } protected void Application_PostAuthenticateRequest() { RecordEvent("PostAuthenticateRequest"); } private void RecordEvent(string name) { List<string> eventList = Application["events"] as List<string>; if (eventList == null) { Application["events"] = eventList = new List<string>(); } eventList.Add(name); } }} |
در اینجا متدی به نام RecordEvent را در کدهای ذکر شده مشاهده میکنید که نام یک رویداد را دریافت و جهت در دسترس قرار دادن در کل برنامه به خاصیت Application از کلاس HttpApplication نسبت داده و متد مذکور را از سه متد دیگر فراخوانی کردهایم. این متدها در زمان رخ دادن رویدادهای BeginRequest, AuthenticateRequest, PostAuthenticateRequest صدا زده خواهند شد.
حال جهت نمایش اطلاعات رویداد نیاز است تغییراتی مشابه ذیل در کنترلر Home ایجاد نماییم.
|
1
2
3
4
5
6
7
8
9
10
11
12
|
using System.Web.Mvc;namespace SimpleApp.Controllers{ public class HomeController : Controller { public ActionResult Index() { return View(HttpContext.Application["events"]); } }} |
ویوی مرتبط با اکشن متد index را مطابق کدهای ذیل بازنویسی میکنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@model List<string>@{ ViewBag.Title = "Events List"; }<h5>Events</h5><table> @foreach (string eventName in Model) { <tr> <td>@eventName</td> </tr> }</table> |
خروجی آن مطابق ذیل خواهد بود :
در این مقاله سعی کردیم ابتدا چرخهی حیات یک Request را فرا گرفته و سپس از طریق فایل global و توسط متدهایی با پیشوند Application_ +نام رویداد (اصطلاحا متدهای ویژه نامیده میشوند) چرخه حیات یک درخواست را مدیریت کنیم.
در Asp.net دو چرخهی حیات مهم وجود دارند که اساس چارچوب MVC را تشکیل میدهند :
- چرخهی حیات برنامه (Application)؛ از لحظهای که برنامه برای اولین بار اجرا میشود و تا لحظهی خاتمهی آن را شامل میشود.
- چرخهی حیات یک درخواست ( Request )؛ مسیری که یک درخواست طی میکند، اصطلاحا PipeLine نامیده میشود که همان چرخهی حیات یک درخواست نیز هست و از لحظهای که درخواست تحویل asp.net شده، تا زمانیکه درخواست ارسال میشود را شامل میشود.
تمرکز بنده بیشتر بر روی روند و مسیری است که یک درخواست طی میکند و قصد دارم با بهره گیری از کتاب Pro Asp.net Mvc 5 و دیگر منابع، چرخهی حیات درخواست را در برنامههای Mvc بررسی کرده و در مقالات آتی ماژولها و هندلرها را بررسی کنم.
در asp.net ، برنامه global فایلهای شامل دو فایل Global.asax , Global.asax.cs است.
فایل Global.asax که هیچ گاه نیاز به ویرایش آن نداریم محتویاتی مانند زیر دارد:
|
1
|
<%@ Application Codebehind="Global.asax.cs" Inherits="YourAppName.MvcApplication" Language="C#" %> |
و صرفا فایل code behind مرتبط را برای asp.net مشخص میکند. این نوع مشخص سازی را از وب فرمها به یاد داریم.
در این مقاله منظور از فایل global فایل Global.asax.cs است که مشتق شده از کلاس System.Web.HttpApplication است:
|
1
2
3
4
5
6
7
|
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { ...// } } |
به صورت پیش فرض کدهای بالا ایجاد شده و کلاس MvcApplication شامل متد Application_Start است که نقطهی شروع چرخهی حیات برنامه را مشخص میکند. اما متد دیگری به نام Application_End() نیز وج ود دارد که در زمان خاتمهی برنامه فراخوانی خواهد شد و فرصتی را برای آزاد سازی منابع اشغال شده توسط برنامه فراهم میآورد .
Asp.net برای پاسخگویی به درخواستهای واصله، وهلههایی از کلاس MvcApplication را میسازد ولی این دو متد صرفا در نقاط شروع و پایان برنامه فراخوانی شده و عملا در وهلههای یاد شده صدا زده نخواهند شد و به جای آنها رویدادهایی را که در ذیل آنها را معرفی میکنیم، فراخوانی شده و چرخهی حیات درخواست را برای ما مشخص میسازند .
: به عنوان اولین رویداد، به محض وصول یک درخواست جدید رخ خواهد داد.
: رویداد AuthenticateRequest برای شناسایی کاربر ارسال کننده درخواست، کاربرد دارد و پس از پردازش کلیهی توابع، رویداد PostAuthenticateRequest صدا زده میشود.
:بههنگام صدور مجوزهای یک درخواست رخ میدهد و مشابه رویداد بالا پس از پردازش کلیهی توابع، رویداد PostAuthorizeRequest صدا زده خواهد شد.
: پس از صدور مجوزهای یک درخواست در رویداد authorization زمانیکه ماژولهای کش میخواهند اطلاعاتی را از کش سرور مطالبه کنند، رخ میدهد و به مانند دو رخداد قبلی، PostResolveRequestCache نیز پس از اتمام پردازش توابع رویداد رخ میدهد.
: زمانی که Asp.net میخواهد هندلری را برای پاسخگویی به درخواست واصله انتخاب کند رخ میدهد و PostMapRequestHandler نیز پس از این انتخاب، تریگر میشود.
: جهت بدست آوردن دادههایی نظیر سشن و ... مرتبط با درخواست جاری کاربرد داشته و PostAcquireRequestState نیز پس از پردازش توابع رویداد رخ خواهد داد.
: بالافاصله قبل و همچنین بلافاصله بعد از این که یک هندلر بخواهد درخواستی را پردازش کند، رخ میدهد. PostRequestHandlerExecute نیز همانند دیگر رویدادهای گذشته، پس از اتمام پردازش توابع، این رویداد رخ خواهد داد.
: زمانی رخ میدهد که دادههای مرتبط با درخواست جاری، در ادامهی روند پردازش درخواست مورد نیاز نباشند و پس از پردازش توابع رویداد، PostReleaseRequestState رخ خواهد داد .
: به جهت اینکه ماژولهای مسئول کش، توانایی به روز رسانی دادههای خود، برای پاسخگویی به درخواستهای بعدی را داشته باشند، این رویداد رخ میدهد.
: قبل از انجام عملیات لاگین برای درخواست جاری رخ میدهد و پس از پردازش توابع رویداد نیز PostLogRequest تریگر میشود.
: پس از پایان کار پردازش درخواست جاری و مهیا شدن پاسخ مرتبط جهت ارسال به مرورگر تریگر خواهد شد.
: قبل از ارسال HTTP headers به مرورگر این رویداد رخ خواهد داد.
: بعد از ارسال شدن هدرها و قبل از ارسال محتوای صفحه به مرورگر رخ میدهد.
: هر زمان و در هر مرحله از پردازش درخواست، چنانچه خطایی صورت پذیرد این رویداد رخ خواهد داد.
فریم ورک Asp.net جهت مدیریت بهتر یک درخواست، در تمام مسیر پردازش، رویدادهای بالا را مهیا کرده است. در ادامه نحوهی هندل کردن رویدادهای چرخهی حیات درخواست را در فایل global توضیح میدهم. هر چند که استفاده از این فایل بدین منظور، صرفا برای مدیریت مسائل ابتدایی مناسب بوده و در یک پروژهی بزرگ موجب به هم ریختگی فایل global با کدهای زیاد و خوانایی پایین بوده که قابلیت استفاده مجدد در دیگر پروژهها را نیز ندارد.
Asp.net این مشکل را با معرفی ماژولها که در مقالات آتی توضیح خواهم داد، مرتفع کرده است.
در فایل global هر گاه متدی را با پیشوند Application_ و نام یکی از رویدادهای بالا بنویسید Asp.net آن را به عنوان هندلری برای رویداد مذکور میشناسد. به عنوان مثال متدی با نام Application_BeginRequest متد رویداد BeginRequest میباشد.
ابتدا یک پروژهی MVC جدید را به نام SimpleApp ایجاد کرده و فایل global آن را مطابق ذیل تغییر میدهیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Web.Mvc;using System.Web.Routing;namespace SimpleApp{ public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RouteConfig.RegisterRoutes(RouteTable.Routes); } protected void Application_BeginRequest() { RecordEvent("BeginRequest"); } protected void Application_AuthenticateRequest() { RecordEvent("AuthenticateRequest"); } protected void Application_PostAuthenticateRequest() { RecordEvent("PostAuthenticateRequest"); } private void RecordEvent(string name) { List<string> eventList = Application["events"] as List<string>; if (eventList == null) { Application["events"] = eventList = new List<string>(); } eventList.Add(name); } }} |
در اینجا متدی به نام RecordEvent را در کدهای ذکر شده مشاهده میکنید که نام یک رویداد را دریافت و جهت در دسترس قرار دادن در کل برنامه به خاصیت Application از کلاس HttpApplication نسبت داده و متد مذکور را از سه متد دیگر فراخوانی کردهایم. این متدها در زمان رخ دادن رویدادهای BeginRequest, AuthenticateRequest, PostAuthenticateRequest صدا زده خواهند شد.
حال جهت نمایش اطلاعات رویداد نیاز است تغییراتی مشابه ذیل در کنترلر Home ایجاد نماییم.
|
1
2
3
4
5
6
7
8
9
10
11
12
|
using System.Web.Mvc;namespace SimpleApp.Controllers{ public class HomeController : Controller { public ActionResult Index() { return View(HttpContext.Application["events"]); } }} |
ویوی مرتبط با اکشن متد index را مطابق کدهای ذیل بازنویسی میکنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@model List<string>@{ ViewBag.Title = "Events List"; }<h5>Events</h5><table> @foreach (string eventName in Model) { <tr> <td>@eventName</td> </tr> }</table> |
خروجی آن مطابق ذیل خواهد بود :
در این مقاله سعی کردیم ابتدا چرخهی حیات یک Request را فرا گرفته و سپس از طریق فایل global و توسط متدهایی با پیشوند Application_ +نام رویداد (اصطلاحا متدهای ویژه نامیده میشوند) چرخه حیات یک درخواست را مدیریت کنیم.
چند روز پیش در حال استفاده از افزونهی jQuery Bootgrid بودم که دادههای خود را در قالب زیر به صورت کوئری استرینگ ارسال میکند.
|
1
|
current=1&rowCount=10&sort[sender]=asc&searchPhrase=&id=b0df282a-0d67-40e5-8558-c9e93b7befed |
|
1
2
3
4
5
6
7
8
9
|
[AttributeUsage(AttributeTargets.Property,Inherited = true)]public class RequestBodyField:Attribute{ public string Field; public RequestBodyField(string field) { this.Field = field; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class EmployeesRequestBody{ [RequestBodyField("current")] public int CurrentPage { get; set; } [RequestBodyField("rowcount")] public int RowCount { get; set; } [RequestBodyField("searchPhrase")] public string SearchPhrase { get; set; } [RequestBodyField("sort")] public NameValueCollection SortDictionary { get; set; }} |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
public T GetFromQueryString<T>() where T : new() { var obj = new T(); var queryString = HttpContext.Current.Request.QueryString; var queries = HttpUtility.ParseQueryString(queryString.ToString()); var properties = typeof(T).GetProperties(); foreach (var property in properties) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { var requestBodyField = attribute as RequestBodyField; if (requestBodyField == null) continue; //get value of query string var valueAsString = queries[requestBodyField.Field]; var converter = TypeDescriptor.GetConverter(property.PropertyType); var value = converter.ConvertFrom(valueAsString); if (value == null) continue; property.SetValue(obj, value, null); } } return obj; } |
|
1
|
HttpContext.Current.Request.QueryString |
سپس در مرحلهی بعدی با استفاده از Reflection پراپرتیهایی را که دارای attribute تعریف شده هستند، پیدا میکنیم.
|
1
2
|
var converter = TypeDescriptor.GetConverter(property.PropertyType);var value = converter.ConvertFrom(valueAsString); |
سپس در صورتی که مقدار صحیح دریافت شود و برابر null نباشد، مقدار را در پراپرتی مربوطه جا میدهیم.
نکتهای که در اینجا نیاز به تلاش بیشتر دارد، کلید sort در کوئری استرینگ است. با نگاهی دقیقتر متوجه میشوید که خود کلید دو مقدار دارد که یکی از مقادیرش با کلید ترکیب شده است. این حالت روش ارسال آرایهها با نام کلیدی متفاوت در کوئری استرینگ است. این حالت ارسال باعث میشود که گرید بتواند حالت multi sort را نیز پیاده سازی کند.
پس برای دریافت این نوع مقادیر کمی کد به آن اضافه میکنیم. برای دریافت مقادیر آرایهای کد زیر را به سیستم اضافه میکنیم:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
if (valueAsString == null) { var keys = from key in queries.AllKeys where key.StartsWith(requestBodyField.Field) select key; var collection = new NameValueCollection(); foreach (var key in keys) { var openBraketIndex = key.IndexOf("[", StringComparison.Ordinal); var closeBraketIndex = key.IndexOf("]", StringComparison.Ordinal); if (openBraketIndex < 0 || closeBraketIndex < 0) throw new Exception("query string is corrupted."); openBraketIndex++; //get key in [...] var fieldName = key.Substring(openBraketIndex, closeBraketIndex - openBraketIndex); collection.Add(fieldName, queries[key] ); } property.SetValue(obj, collection, null); continue; } |
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
public T GetFromQueryString<T>() where T : new(){ var obj = new T(); var properties = typeof(T).GetProperties(); var queryString = HttpContext.Current.Request.QueryString; var queries = HttpUtility.ParseQueryString(queryString.ToString()); foreach (var property in properties) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { var requestBodyField = attribute as RequestBodyField; if (requestBodyField == null) continue; //get value of query string var valueAsString = queries[requestBodyField.Field]; if (valueAsString == null) { var keys = from key in queries.AllKeys where key.StartsWith(requestBodyField.Field) select key; var collection = new NameValueCollection(); foreach (var key in keys) { var openBraketIndex = key.IndexOf("[", StringComparison.Ordinal); var closeBraketIndex = key.IndexOf("]", StringComparison.Ordinal); if (openBraketIndex < 0 || closeBraketIndex < 0) throw new Exception("query string is corrupted."); openBraketIndex++; //get key in [...] var fieldName = key.Substring(openBraketIndex, closeBraketIndex - openBraketIndex); collection.Add(fieldName, queries[key]); } property.SetValue(obj, collection, null); continue; } var converter = TypeDescriptor.GetConverter(property.PropertyType); var value = converter.ConvertFrom(valueAsString); if (value == null) continue; property.SetValue(obj, value, null); } } return obj;} |
حال به صورت زیر این متد را صدا میزنیم:
|
1
2
3
4
|
public virtual ActionResult GetEmployees(){ var request = new Requests().GetFromQueryString<EmployeesRequestBody>();} |
پس اگر قصد توسعه SPA با هر فریمورکی مثل angular را داشته باشید، این را در نظر داشته باشید که دیر یا زود هنگام استفاده از افزونههای جیکوئری به مشکل برخواهید خورد.
به هنگام توسعهی برنامه با استفاده از فریم ورکهای SPA، امکانات توکار ASP.NET MVC مثل اعتبارسنجی یکپارچه و strongly typed viewها را از دست خواهید داد. شاید یک سری پروژه در Github پیدا کنید که سعی کردهاند اینها را با یکدیگر سازگار کنند. اما به محض استفاده متوجه میشوید که اگر همهی کارها را خودتان با Angular انجام بدهید راحتتر هستید تا استفاده از کتابخانههای آزمایشی و ناقص.
البته باز هم نمیگویم که اینها تقصیر AngularJS است. ذات توسعهی SPAها، این گونه است و در توسعهی SPA با هر فریمورکی به این مشکلات برخواهید خورد.
حال که یکسری مشکلات عمومی را بررسی کردیم، بدنیست نگاهی اختصاصی به خود AngularJS بیندازیم.
اگر به تعدای از لینکهای سایت ihateangular مراجعه کنید میبینید که هر کسی نظری دارد: یکی میگوید به هیچ وجه Directive ننویسید، یکی دیگر میگوید کنترلر ننویسید و تمامی کارها را در directiveهای سفارشی نوشته شده توسط خودتان انجام بدهید، کلا همه جا علیه performance این فریمورک صحبت میکنند و همگی به پیچیده بودن آن اذعان دارند.
AngularJS فریمورک خیلی خوبی برای نوشتن برنامههای تست پذیر است و کسی منکر قابلیتهای آن نیست. ولی این را نیز در نظر بگیرید که برای تست پذیر بودن، خیلی چیزها از جمله سادگی کار را از دست میدهید. معمولا میگویند که AngularJS کارهای مشکل را ساده میکند و کارهای ساده را مشکل.
پیشنهاد من این است که اگر هنوز AngularJS را فرا نگرفتهاید، حداقل یادگیری آن را تا انتشار نسخهی 2 آن به تعویق بیندازید. اگر AngularJS را بلد هستید، دیگر آن را در پروژهای استفاده نکنید؛ چون دیگر کدهای شما در نسخهی 2 کار نخواهد کرد و احتیاج به انجام تغییرات گستردهای در کدهای نوشته شده قبلی پیدا میکنید.
چند سالی ست که شبکههای اجتماعی رشد چشمگیری در دنیای مجازی داشتهاند و به حرف اول و پیشتاز آن بدل شدهاند. این شبکهها در همهی زمینهها از معرفی و فروش محصولات گرفته تا معرفی سایت و وبلاگ و ... بکار گرفته میشوند و لذا مقولهی بهینه سازی یک وب سایت برای این شبکههای اجتماعی امری ناگزیر است. مباحث سئو که پیرامون بهینه سازی یک وب سایت برای موتورهای جستجو میباشند، امروزه با پدیدهی جدید دیگری آمیخته شدهاند و به تعاریف قدیمی و معمول، گزینههایی افزوده شده است. از اینرو شناخت و بکارگیری روشهای بهینه، برای این مباحث ضروری میباشند.
همانطور که میدانید هنگامیگه در ادیتور ارسال مطلب یکی از شبکههای اجتماعی (فیسبوک ، توییتر ، گوگل پلاس و ...) آدرس سایتی را وارد میکنید، بلافاصله لینک پردازش شده و پیش نمایشی از آن وبسایت در ادیتور نمایش داده میشود. برخی پیش نمایشها حاوی عکس، عنوان لینک و خیلی منظم هستند و برخی دیگر فقط نام و عنوان سایت را در برمیگیرند.
برای نمونه به تصاویر زیر دقت کنید:
تصویر فوق مربوط به لینک یک سایت خبری در ادیتور فیسبوک میباشد. همانطور که میبینید عنوان خبر، عکس و توضیح مختصری در مورد خبر، با نظم و ترتیبی خاص نمایش داده شده است.
حال به تصویر زیر که مربوط به لینکی از همین سایت میباشد دقت کنید:
همانطور که میبینید، تنها لینکی ساده، بدون هیچ پیش نمایشی از وب سایت نشان داده شده است.
دلیل این اتفاق وجود یا عدم وجود متاتگهایی که Social Media Metadata نامیده میشوند میباشند. چنانچه وب سایتی بوسیلهی این متاتگها برای شبکههای اجتماعی بهینه سازی شده باشد، با قرار دادن هر لینکی در ادیتور شبکههای اجتماعی، پیش نمایشی خوب از آن مطلب به نمایش گذاشته میشود. اهمیت این متاتگها در سایتهای خبری، فروشگاهها، سایتهای آموزشی و ... بسیار بالا میباشد تا از مزایای جذب کاربر توسط شبکههای اجتماعی بهرهمند شوند. با این مقدمه میرویم به سراغ معرفی و چگونگی بکارگیری این متاتگ ها.
ابتدا تگ html خود را به شکل زیر تغییر دهید:
|
1
|
<html itemscope itemtype="http://schema.org/Article"> |
برای شناخت و اهمیت بکارگیری خصوصیت itemscope میتوانید اینجا را ببینید. این خصوصیت زوجهای کلید/مقداری را تعریف میکند که جزئی از سند میباشند. در ادامه itemtype به تشریح یک زوج کلید/مقدار میپردازد که به مرورگر میگوید کدام آیتمها برای یک مقاله بهینه شدهاند.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<title>عنوان صفحه ، حداکثر 60 تا 70 کارکتر باشد</title><meta name="description" content="شرح صفحه ، حداکثر 150 کارکتر باشد" /> <!-- Schema.org markup for Google+ --><meta itemprop="name" content="نام یا عنوان صفحه"><meta itemprop="description" content="شرح صفحه"><meta itemprop="image" content="نشانی اینترنتی عکسی که در پیشنمایش نشان داده میشود"> <!-- Twitter Card data --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:site" content="کپی رایت نام سایت"><meta name="twitter:title" content="نام یا عنوان صفحه"><meta name="twitter:description" content="شرح صفحه"><meta name="twitter:creator" content="نویسنده"><!-- Picture size at least 280x150px -->عکس پیشنمایش با ابعاد حداقل <meta name="twitter:image:src" content="نشانی اینترنتی عکس مطلب"> <!-- Open Graph data --><meta property="og:title" content="عنوان صفحه" /><meta property="og:type" content="article" /><meta property="og:url" content="نشانی سایت" /><meta property="og:image" content="نشانی عکس مطلب" /><meta property="og:description" content="شرح مطلب" /><meta property="og:site_name" content="نام سایت" /><meta property="article:published_time" content="تاریخ انتشار" /><meta property="article:modified_time" content="تاریخ بروزرسانی" /><meta property="article:section" content="نام بخش محتوی متن مقاله" /><meta property="article:tag" content="نام تگ محتوی متن مقاله" /><meta property="fb:admins" content="شناسه عددی کاربری شما در فیسبوک" /> |
ابتدا تگ html خود را به شکل زیر تغییر دهید:
|
1
|
<html itemscope itemtype="http://schema.org/Product"> |
و در نهایت متاتگهای زیر را به صفحه خود اضافه کنید:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<title>عنوان صفحه</title><meta name="description" content="شرح صفحه" /><!-- Schema.org markup for Google+ --><meta itemprop="name" content="عنوان صفحه"><meta itemprop="description" content="Tشرح صفحه"><meta itemprop="image" content="نشانی عکس محصول یا کالا"><!-- Twitter Card data --><meta name="twitter:card" content="product"><meta name="twitter:site" content="کپی رایت سایت"><meta name="twitter:title" content="عنوان صفحه"><meta name="twitter:description" content="شرح محصول یا کالا"><meta name="twitter:creator" content="نویسنده"><meta name="twitter:image" content="نشانی عکس محصول یا کالا"><meta name="twitter:data1" content="قیمت محصول یا کالا"><meta name="twitter:label1" content="Price"><meta name="twitter:data2" content="رنگ کالا یا محصحول"><meta name="twitter:label2" content="Color"><!-- Open Graph data --><meta property="og:title" content="عنوان صفحه" /><meta property="og:type" content="article" /><meta property="og:url" content="نشانی سایت" /><meta property="og:image" content="عکس محصول یا کالا" /><meta property="og:description" content="شرح مصحول" /><meta property="og:site_name" content="نام سایت" /><meta property="og:price:amount" content="قیمت محصول یا کالا" /><meta property="og:price:currency" content="واحد ارزی قیمت" /> |