چه نوع اپلیکیشنهای را میتوان با Node.js توسعه داد؟
- سرور WebSocket جهت توسعهی اپلیکیشنهای بلادرنگ
- فایل آپلودر سریع در سمت کلاینت
- Ad Server
- و ...
var contents = fs.readFileSync('filePath'); console.log(content); console.log('Doing something else');
fs.readFile('filePath', function (err, contents) { console.log(contents); }); console.log('Doing something else');
برای شروع به کار با Node.js میتوانید با مراجعه به وبسایت رسمی آن، آنرا دانلود و بر روی سیستم خود نصب کنید. بعد از نصب Node میتوانیم از طریق command line وارد shell آن شوید و دستورات جاوا اسکریپتی خود را اجرا نمائید:
احتمالاً به این نوع استفادهی از Node.js که به REPL معروف است، نیازی نداشته باشید. در واقع هدف بررسی نصب بودن رانتایم بر روی سیستم است. با استفاده از فرمان node نیز میتوان یک فایل جاوا اسکریپتی را اجرا کرد. برای اینکار یک فایل با نام test.js را با محتویات زیر درون VS Code ایجاد کنید:
سپس دستور node test.js را وارد کنید:
همانطور که مشاهده میکنید نتیجهی فایل عنوان شده، در خروجی نمایش داده شده است. در حالت کلی تمام کاری که نود انجام میدهد، ارائه یک Execution engine برای جاوا اسکریپت میباشد.
استفاده از Node.js در ویژوال استودیو
برای کار با Node.js درون ویژوال استودیو باید ابتدا افزونهی Node.js Tools را برای ویژوال استودیو نصب کنید. بعد از نصب این افزونه، تمپلیت Node.js در زمان ایجاد یک پروژه برای شما نمایش داده خواهد شد:
برای شروع، تمپلیت Blank Node.js Console Application را انتخاب کرده و بر روی OK کلیک کنید. با اینکار یک پروژه با ساختار زیر برایمان ایجاد خواهد شد:
همانطور که ملاحظه میکنید، یک فایل با نام app.js درون تمپلیت ایجاد شده، موجود است. app.js در واقع نقطهی شروع برنامهمان خواهد بود. همچنین دو فایل دیگر نیز با نامهای README.md، جهت افزودن توضیحات و یک فایل با نام package.json، جهت مدیریت وابستگیهای برنامه به پروژه اضافه شدهاند. اکنون میتوانیم شروع به توسعهی برنامهی خود درون ویژوال استودیو کنیم. همچنین میتوانیم از قابلیتهای debugging ویژوال استودیو نیز بهره ببریم:
اگر مسیر پروژهی ایجاد شدهی فوق را درون windows explorer باز کنید خواهید دید که ساختار آن شبیه به یک پروژهی Node.js میباشد. با این تفاوت که دو آیتم دیگر همانند دیگر پروژههای ویژوال استودیو نیز به آن اضافه شده است که طبیعتاً میتوانید در حین کار با سورس کنترل، از انتشار آنها صرفنظر کنید.
لازم به ذکر است پروژهی ایجاد شدهی فوق را نیز میتوانید همانند حالت عادی، از طریق command line و همانند پروژههای Node.js اجرا کنید:
node app.js
در واقع از ویژوال استدیو میتوانیم به عنوان یک ابزار برای دیباگ پروژههای Node.js استفاده کنیم. لازم به ذکر است، Visual Studio Code نیز امکان دیباگ اپلیکیشنهای Node.js را در اختیارمان قرار میدهد. در نتیجه در مواقعیکه نسخهی کامل ویژوال استودیو در دسترس نیست نیز میتوانیم از VS Code برای دیباگ برنامههایمان استفاده کنیم:
آشنایی با امکانات بستهی angular/platform-browser@
در ماژول angular/platform-browser@، دو سرویس Meta و Title، امکان تغییر پویای متاتگهای صفحهی جاری را مهیا میکنند. برای نمونه فرض کنید قصد دارید یک چنین متاتگهایی را به صفحهی جاری اضافه کنید:
<head> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <title>newTitle ...</title> <base href="/"> <meta name="description" content="Angular meta service"> <meta name="author" content="DNT"> <meta name="keywords" content="Angular, Meta Service"> <meta name="twitter:card" content="summary"> <meta name="twitter:site" content="@my_site"> <meta name="twitter:title" content="Front-end Web Development"> <meta name="twitter:description" content="Learn frontend web development..."> <meta name="twitter:image" content="https://site/images/image.png"> <meta name="author" content="Other Author"> </head>
import { Component, OnInit } from "@angular/core"; import { Meta, Title } from "@angular/platform-browser"; @Component({ selector: "app-seo-tests", templateUrl: "./seo-tests.component.html", styleUrls: ["./seo-tests.component.css"] }) export class SeoTestsComponent implements OnInit { constructor(private metaService: Meta, private titleService: Title) { }
افزودن یک یا چند متاتگ
متد addTag سرویس Meta، کار افزودن پویای یک متا تگ جدید را به همراه ویژگیهای name و content آن، انجام میدهد. در ذیل چندین مثال از آنرا مشاهده میکنید. در اینجا یا میتوان از متد addTag استفاده کرد که تنها یک متاتگ را به صفحه اضافه میکند و یا از متد addTags کمک گرفت که میتواند آرایهای از متاتگها را به صورت پویا به صفحهی جاری اضافه کند:
// addTag & addTags this.metaService.addTag({ name: "description", content: "How to optimize your Angular App for search engine and other crawlers." }); this.metaService.addTag({ name: "author", content: "DNT" }); this.metaService.addTag({ name: "keywords", content: "Angular, Meta Service" }); // Or this.metaService.addTags([ { name: "description", content: "How to optimize your Angular App for search engine and other crawlers." }, { name: "author", content: "DNT" }, { name: "keywords", content: "Angular, Meta Service" } ], false); // --> forceCreation = false this.metaService.addTag({ name: "twitter:card", content: "summary_large_image" }); this.metaService.addTag({ name: "twitter:site", content: "@my_site" }); this.metaService.addTag({ name: "twitter:title", content: "Front-end Web Development" }); this.metaService.addTag({ name: "twitter:description", content: "Learn frontend web development..." }); this.metaService.addTag({ name: "twitter:image", content: "https://site/images/image.png" }); // Or this.metaService.addTags([ { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:site", content: "@my_site" }, ], false); // --> forceCreation = false
دریافت محتوای متاتگهای موجود
با استفاده از متد getTag میتوان یک متاتگ مشخص را به صورت HTMLMetaElement دریافت کرد:
// getTag & getTags const viewport = this.metaService.getTag("name=viewport"); if (viewport) { console.log(viewport.content); // width=device-width, initial-scale=1 } const author = this.metaService.getTag("name=author"); if (author) { console.log(author.content); // DNT } this.metaService.addTag({ name: "author", content: "DNT" }); this.metaService.addTag({ name: "author", content: "Other Author" }, true); const authors = this.metaService.getTags("name=author"); console.log(authors[0]); // <meta name="author" content="DNT"> console.log(authors[1]); // <meta name="author" content="Other Author">
به روز رسانی متاتگهای موجود
میتوان از متد updateTag برای تغییر محتوای متاتگی موجود، استفاده کرد:
// updateTag this.metaService.addTag({ name: "twitter:card", content: "summary_large_image" }); this.metaService.updateTag({ name: "twitter:card", content: "summary" }, `name='twitter:card'`); this.metaService.updateTag({ name: "description", content: "Angular meta service" });
حذف تگهای موجود
در اینجا میتوان از دو متد removeTag که یک attribute-selector را دریافت میکند و یا removeTagElement که یک HTMLMetaElement را توسط متد getTag دریافت میکند، برای حذف کامل این تگها استفاده کرد:
// removeTag & removeTagElement this.metaService.removeTag("charset"); // Or const chartsetTag = this.metaService.getTag("charset"); if (chartsetTag) { this.metaService.removeTagElement(chartsetTag); }
تنظیم عنوان صفحهی جاری
سرویس توکار دیگری به نام Title امکان تغییر عنوان صفحهی جاری را به صورت پویا میسر میکند:
// Setting the browser page Title in an Angular app const currentTitle = this.titleService.getTitle(); console.log(currentTitle); this.titleService.setTitle("newTitle ...");
طراحی سرویسی برای افزودن پویای متاتگها به صفحات مختلف سایت
میتوان شبیه به مطلب «نمایش Breadcrumbs در برنامههای Angular» به قسمت data مسیریابی، اطلاعات عنوان صفحه و همچنین metaTags آنرا اضافه کرد:
const routes: Routes = [ { path: "seo", component: SeoTestsComponent, data: { title: "Page Title", metaTags: { description: "Page Description or some content here", keywords: "some, keywords, here, separated, by, a comma" } } } ];
به همین جهت سرویس SEO را در مسیر src\app\core\seo-service.ts به صورت ذیل ایجاد میکنیم:
import { Injectable } from "@angular/core"; import { Title, Meta } from "@angular/platform-browser"; import { Router, NavigationEnd, ActivatedRouteSnapshot } from "@angular/router"; @Injectable() export class SeoService { constructor(private titleService: Title, private metaService: Meta, private router: Router) { } enableSeo() { this.router.events .filter(event => event instanceof NavigationEnd) .distinctUntilChanged() .subscribe(() => { this.addMetaData(this.router.routerState.snapshot.root); }); } private addMetaData(root: ActivatedRouteSnapshot): void { if (root.children && root.children.length) { this.addMetaData(root.children[0]); } else if (root.data) { this.setTitle(root.data); this.setMetaTags(root.data); } } private setMetaTags(routeData: { [name: string]: any; }) { const routeDataMetaTagsKey = "metaTags"; const metaTags = routeData[routeDataMetaTagsKey]; if (!metaTags) { return; } for (const tag in metaTags) { if (metaTags.hasOwnProperty(tag)) { const newTag = { name: tag, content: metaTags[tag] }; console.log("new tag", newTag); this.metaService.addTag(newTag); } } } private setTitle(routeData: { [name: string]: any; }) { const routeDataTitleKey = "title"; const title = routeData[routeDataTitleKey]; if (title) { console.log("new title", title); this.titleService.setTitle(title); } } }
در اینجا در ابتدای کار مشترک رخداد NavigationEnd سیستم مسیریابی خواهیم شد:
this.router.events .filter(event => event instanceof NavigationEnd) .distinctUntilChanged() .subscribe(() => { this.addMetaData(this.router.routerState.snapshot.root); });
سپس در این متد خاصیت data مسیرنهایی را خوانده و کلیدهای title و metaTags آنرا استخراج میکنیم و سپس توسط متدهای this.titleService.setTitle و this.metaService.addTag، این عنوان و متاتگهای جدید را به صورت پویا به صفحه اضافه خواهیم کرد.
پس از تعریف این سرویس، برای معرفی آن به برنامه، ابتدا آنرا به قسمت providers مربوط به CoreModule اضافه میکنیم:
import { SeoService } from "./seo-service"; @NgModule({ providers: [ SeoService ] }) export class CoreModule {}
import { SeoService } from "./core/seo-service"; export class AppComponent { constructor(private seoService: SeoService) { this.seoService.enableSeo(); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
متدهای الحاقی و ترکیب کنندههای اعمال غیرهمزمان
متد WhenAll
کار آن ترکیب تعدادی Task است و اجرای آنها. تنها زمانی خاتمه مییابد که کلیهی Taskهای معرفی شده به آن خاتمه یافته باشند. هدف از آن اجرای همزمان و مستقل چندین Task است. برای مثال دریافت چندین فایل به صورت همزمان از اینترنت.
همچنین باید دقت داشت که در اینجا، هر Task کاری به نتایج Taskهای دیگر ندارد و کاملا مستقل اجرا میشود. اگر نیاز است Taskها مستقل اجرا شوند، از همان روش سریالی اجرای Taskها، توسط معرفی هر کدام به کمک await استفاده کنید.
به علاوه اگر در این بین استثنایی وجود داشته باشد، تنها پس از پایان عملیات تمام Taskها بازگشت داده میشود. این استثناء نیز از نوع Aggregate Exception است.
using System.Linq; using System.Threading.Tasks; namespace Async07 { public class EggBoiler { private const int BoilingTimeMs = 200; private static Task boilEgg() { var bolingTask = Task.Run(() => { Task.Delay(BoilingTimeMs); }); return bolingTask; } public async Task BoilEggsSequentialAsync(int count) { for (var i = 0; i < count; i++) { await boilEgg(); } } public async Task BoilEggsSimultaneousAsync(int count) { var tasksList = from egg in new[] { 1, 2, 3, 4, 5 } select boilEgg(); await Task.WhenAll(tasksList); // ... } } }
اما در متد BoilEggsSimultaneousAsync به علت بکارگیری Task.WhenAll پختن تمام تخم مرغهای مدنظر همزمان آغاز میشود و تا پایان عملیات (پخته شدن تمام تخم مرغها) صبر خواهد شد.
متد WhenAny
در حالت استفاده از متد WhenAny، هر کدام از Taskهای در حال پردازش که خاتمه یابند، کل عملیات خاتمه خواهد یافت. فرض کنید نیاز دارید تا دمای کنونی هوای منطقهی خاصی را از چند وب سرویس مختلف دریافت کنید. میتوان در این حالت تمام اینها را توسط WhenAny ترکیب کرد و هر کدام که زودتر خاتمه یابد، عملیات را پایان خواهد داد.
public class Downloader { private Task<string> downloadTask(string url) { return new WebClient().DownloadStringTaskAsync(url); } public async Task<int> GetTemperature() { var sites = new[] { "http://www.site1.com/svc", "http://www.site2.com/svc", "http://www.site3.com/svc", }; var tasksList = from site in sites select downloadTask(site); try { var finishedTask = await Task.WhenAny(tasksList); var result = await finishedTask; } catch (Exception ex) { } // todo: process result, get temperature return 10; // for example. } }
در این حالت اگر نیاز بود وضعیت سایر Taskها، مثلا در صورت شکست آنها، بررسی شوند، میتوان از یکی از دو قطعه کد زیر استفاده کرد:
foreach (var task in tasksList) { var ignored = task.ContinueWith( t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted); } // or foreach (var task in tasksList) { var ignored = task.ContinueWith( t => { if (t.IsFaulted) Console.WriteLine(t.Exception); }); }
کاربرد دیگر WhenAny زمانی است که برای مثال میخواهید تعداد زیادی Url را پردازش کنید، اما نمیخواهید برای نمایش اطلاعات، تا پایان عملیات تمامی آنها مانند WhenAll صبر کنید. میخواهید به محض پایان کار یکی از Taskها، عملیات نمایش نتیجهی آنرا انجام دهید:
public async Task ShowTemperatures() { var sites = new[] { "http://www.site1.com/svc", "http://www.site2.com/svc", "http://www.site3.com/svc", }; var tasksList = sites.Select(site => downloadTask(site)).ToList(); while (tasksList.Any()) { try { var tempTask = await Task.WhenAny(tasksList); tasksList.Remove(tempTask); var result = await tempTask; //todo: show result } catch(Exception ex) { } } }
کاربرد سوم WhenAny کنترل تعداد وظایف همزمان است. برای مثال اگر قرار است هزاران تصویر از اینترنت دریافت شوند، نباید تمام وظایف را یکجا راه اندازی کرد. شاید نیاز باشد هربار فقط 15 وظیفهی همزمان عمل کنند و نه بیشتر. در این حالت، مثال قبلی دارای یک حلقهی کنترل کننده tasksList ارائه شده خواهد شد. هر بار تعداد معینی وظیفه به tasksList اضافه و پردازش میشوند و این روند تا پایان کار تعداد Urlها ادامه خواهد یافت (یک Take و Skip است؛ مانند صفحه بندی اطلاعات).
متدهای Run و FromResult
متد Task.Run اضافه شده در دات نت 4.5 به این معنا است که میخواهید Task ایجاد شده بر روی Thread pool اجرا شود. پارامتر آن میتواند یک delegate یا عبارت lambda و یا حتی یک Task باشد. خروجی آن نیز یک Task است و به همین جهت با async و await سی شارپ 5 سازگاری بهتری دارد.
استفاده از Task.Run نسبت به عملیات Threading متداول کارآیی بهتری دارد، زیرا ایجاد Threadهای جدید زمانبر بوده و زمانیکه به صورت خودکار از Thread pool استفاده میشود، تا حد امکان، استفادهی مجدد از تردهای بیکار در حال حاضر، مدنظر است.
متد Task.FromResult کار بازگشت یک Task را از نتایج متدهای مختلف فراهم میکند. فرض کنید یک متد async تعریف کردهاید که خروجی آن Task of T است. در اینجا اگر داخل متد، از یک متد معمولی که یک عدد int را ارائه میدهد استفاده کنیم، با استفاده از Task.FromResult بلافاصله میتوان یک Task of int را بازگشت داد.
متد Delay
پیشتر برای به خواب فرو بردن یک ترد از متد Thread.Sleep استفاده میشد. کار Thread.Sleep بلاک کردن ترد جاری است. در دات نت 4.5، بجای آن باید از Task.Delay استفاده شود که یک مکانیزم غیر قفل کننده را جهت صبر کردن به همراه بازگشت یک Task، ارائه میدهد.
یکی از کاربردهای Delay منهای صبر کردن تا مدت زمانی مشخص، ایجاد مکانیزم timeout است. برای مثال حالت Task.WhenAny را درنظر بگیرید. اگر در اینجا timeout مدنظر ما 3 ثانیه باشد، میتوان یکی از Taskها را Task.Delay با آرگومان مساوی 3000 معرفی کرد. اگر هر کدام از taskهای تعریف شده زودتر از 3 ثانیه پایان یافتند که بسیار خوب؛ در غیر اینصورت Task.Delay معرفی شده کار را تمام میکند.
متد Yield
متد Task.Yield بسیار شبیه به متد قدیمی DoEvents است که از آن برای اجازه دادن به سایر اعمال جهت اجرا، در بین یک عمل طولانی، استفاده میشد.
متد ConfigureAwait
به صورت پیش فرض ادامه یک عملیات همزمان، بر روی ترد ایجاد کنندهی آن اجرا میشود. برای نمونه اگر یک عملیات async در ترد UI آغاز شود، نتیجهی آن نیز در همان ترد UI بازگشت داده میشود. به این ترتیب دیگر نیازی نخواهد بود تا نگرانی در مورد نحوهی دسترسی به مقدار آن توسط عناصر UI داشته باشیم.
اگر به این مساله اهمیت نمیدهید، برای مثال اگر اعمال در حال انجام، کاری به عناصر UI ندارند، از متد ConfigureAwait با پارامتر false بر روی یک task پیش از فراخوانی await بر روی آن، استفاده کنید.
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
به صورت خلاصه در سی شارپ 5
- بجای task.Wait قدیمی، از await task برای صبر کردن تا پایان یک task استفاده کنید.
- بجای task.Result جهت دریافت یک نتیجهی یک task از await task کمک بگیرید.
- بجای Task.WaitAll از await Task.WhenAll و بجای Task.WaitAny از await Task.WhenAny استفاده نمائید.
- همچنین Thread.Sleep در اعمال async با await Task.Delay جایگزین شدهاست.
- در اعمال غیرهمزمان همیشه متد ConfigureAwait false را بکار بگیرید، مگر اینکه به Context نهایی آن واقعا نیاز داشته باشید.
و برای ایجاد یک Task جدید از Task.Run یا TaskFactory.StartNew استفاده نمائید.
وبلاگها و سایتهای ایرانی
Visual Studio
امنیت
ASP. Net
- مثالی از نحوه استفاده از کنترلهای جدید چارت مایکروسافت و همچنین مثالی دیگر و مثال بعدی!
طراحی وب
اسکیوال سرور
سیشارپ
عمومی دات نت
متفرقه
- نحوه به تاخیر انداختن ارسال تمامی ایمیلها در آوت لوک (جدا مهم است! به شخصه این مشکل ارسال زود هنگام ایمیل را چندین بار داشتهام!)
فرض کنید یک دیتابیس آزمایشی دارید که میخواهید تمام رکوردهای آنرا حذف کنید. اگر در این دیتابیس انواع و اقسام کلیدهای خارجی و تریگر و امثال آن وجود داشته باشند، صرفا با یک دستور delete ساده کار به پایان نمیرسد و موفق به حذف رکوردها نخواهید شد (چون این قید و بندها به همین جهت طراحی شدهاند تا یکپارچگی دیتابیس حفظ شود).
اما اگر واقعا این قیود در این لحظه مهم نبودند و نیاز بود تا تمام رکوردها را حذف کنیم، سریعترین راه حل موجود چیست؟
--Disable Constraints & Triggers
exec sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'
exec sp_MSforeachtable 'ALTER TABLE ? DISABLE TRIGGER ALL'
--Perform delete operation on all table for cleanup
exec sp_MSforeachtable 'DELETE ?'
--Enable Constraints & Triggers again
exec sp_MSforeachtable 'ALTER TABLE ? CHECK CONSTRAINT ALL'
exec sp_MSforeachtable 'ALTER TABLE ? ENABLE TRIGGER ALL'
--Reset Identity on tables with identity column
exec sp_MSforeachtable 'IF OBJECTPROPERTY(OBJECT_ID(''?''), ''TableHasIdentity'') = 1 BEGIN DBCC CHECKIDENT (''?'',RESEED,0) END'
اسکریپت فوق تمامی رکوردهای دیتابیس جاری را حذف کرده و همچنین فیلدهای identity را نیز مجددا به حالت اول باز میگرداند.
توضیحات:
sp_Msforeachtable یکی از رویههای ذخیره شدهی سیستمی اس کیوال سرور میباشد که مستند نشده است. اگر نیاز داشتید کدی را بدون نوشتن یک کرسر و امثال آن، بر روی تمامی جداول اجرا کنید میتوان از آن استفاده نمود.
امضای این رویه ذخیره شده به صورت زیر است:
exec @RETURN_VALUE=sp_MSforeachtable @command1, @replacechar, @command2,
@command3, @whereand, @precommand, @postcommand
که در آن:
RETURN_VALUE مقدار بازگشتی است.
Command1 اولین دستوری است که اجرا خواهد شد (به همین ترتیب سپس Command2 و بعد از آن Command3 اجرا خواهد گردید)
Replacechar کاراکتری است که در دستور T-SQL مورد نظر جایگزین نام جدول خواهد شد. مقدار پیش فرض آن ? است.
Precommand پیش از پردازش عملیات روی هر جدولی اجرا میشود.
Postcommand پس از اجرای کلیه دستورات روی تمامی جداول، اجرا خواهد شد.
چند مثال:
نمایش تمامی جداول دیتابیس جاری
EXEC sp_MSforeachtable "print '?'"
EXEC sp_msforeachtable 'sp_spaceused ''?'''
اصول نمایش Popover در Twitter bootstrap
PopOverها نیز یکی دیگر از کامپوننتهای جاوا اسکریپتی مجموعه بوت استرپ هستند. بسیار شبیه به Tooltip بوده، اما ماندگارتر هستند. PopOverها با کلیک بر روی یک عنصر باز شده و تنها با کلیک مجدد بر روی آن المان، بسته میشوند (البته این موارد نیز قابل تنظیم هستند).
<a rel="popover" data-content="محتوایی برای نمایش" data-original-title="عنوان" href="#">اطلاعات</a> <script type="text/javascript"> $(document).ready(function () { $("[rel='popover']").popover({ placement: 'left' }) .click(function (e) { e.preventDefault(); }); }); </script>
تبدیل خطاهای اعتبارسنجی ASP.NET MVC به PopOver
هدف ما در اینجا نهایتا رسیدن به شکل زیر میباشد:
همانطور که ملاحظه میکنید، اینبار بجای نمایش خطاها در یک برچسب، مقابل کنترل متناظر، این خطا صرفا در حالت فوکوس کنترل، به شکل یک PopOver در کنار آن ظاهر شده است.
کدهای کامل View برنامه
@model Mvc4TwitterBootStrapTest.Models.User @{ ViewBag.Title = "Index"; } @using (Html.BeginForm()) { @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) <fieldset class="form-horizontal"> <legend>تعریف کاربر جدید</legend> <div class="control-group"> @Html.LabelFor(model => model.Name, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.Name) @*@Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })*@ </div> </div> <div class="control-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.LastName) @*@Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })*@ </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"> ارسال</button> </div> </fieldset> } @section JavaScript { <script type="text/javascript"> $.validator.setDefaults({ showErrors: function (errorMap, errorList) { this.defaultShowErrors(); //اگر المانی معتبر است نیاز به نمایش پاپ اور ندارد $("." + this.settings.validClass).popover("destroy"); //افزودن پاپ اورها for (var i = 0; i < errorList.length; i++) { var error = errorList[i]; $(error.element).popover({ placement: 'left' }) .attr("data-original-title", "خطای اعتبارسنجی") .attr("data-content", error.message); } }, // همانند قبل برای رنگی کردن کل ردیف در صورت عدم اعتبار سنجی و برعکس highlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).addClass(errorClass).removeClass(validClass); } else { $(element).addClass(errorClass).removeClass(validClass); $(element).closest('.control-group').removeClass('success').addClass('error'); } $(element).trigger('highlited'); }, unhighlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).removeClass(errorClass).addClass(validClass); } else { $(element).removeClass(errorClass).addClass(validClass); $(element).closest('.control-group').removeClass('error').addClass('success'); } $(element).trigger('unhighlited'); } }); //برای حالت پست بک از سرور عمل میکند $(function () { $('form').each(function () { $(this).find('div.control-group').each(function () { if ($(this).find('span.field-validation-error').length > 0) { $(this).addClass('error'); } }); }); }); </script> }
توضیحات
- با توجه به اینکه دیگر نمیخواهیم خطاها به صورت برچسب در مقابل کنترلها نمایش داده شوند، کلیه Html.ValidationMessageFor به صورت کامنت درآورده شدهاند.
- تغییر دوم مطلب جاری، اضافه شدن متد showErrors به تنظیمات پیش فرض jQuery Validator است. در این متد، اگر المانی معتبر بود، Popover آن حذف میشود یا در سایر حالات، المانهایی که نیاز به اعتبارسنجی سمت کلاینت دارند، یافت شده و سپس ویژگی data-content با مقداری معادل خطای اعتبارسنجی متناظر، به این المان افزوده و سپس متد popover بوت استرپ بر روی آن فراخوانی میگردد.
به عبارتی زمانیکه یک input box در ASP.NET MVC به همراه مقادیر مرتبط با اعتبارسنجی آن رندر میشود، چنین شکلی را خواهد داشت:
<input class="text-box single-line" data-val="true" data-val-required="لطفا نام را تکمیل کنید" id="Name" name="Name" type="text" value="" />
<input class="text-box single-line input-validation-error" data-val="true" data-val-required="لطفا نام را تکمیل کنید" id="Name" name="Name" type="text" value="" data-original-title="خطای اعتبارسنجی" title="" data-content="لطفا نام را تکمیل کنید">
البته این موارد را در صورت نیاز به صورت دستی نیز میتوان تعریف و اضافه کرد:
@Html.TextBoxFor(x => x.Name, new { data_content = "Name is required", data_original_title = "Error", rel="popover" })
<input dir="ltr" class="form-control input-validation-error" type="email" data-val="true" data-val-email="'آدرس ایمیل' is not a valid email address." data-val-remote="این آدرس ایمیل هم اکنون مورد استفادهاست" data-val-remote-url="/Home/ValidateUniqueEmail" data-val-required="'آدرس ایمیل' must not be empty." id="Email" name="Email" >
افزودن آدرس ایمیل به مدل کاربران
به همان مدل قسمت قبل، قصد داریم خاصیت آدرس ایمیل را هم اضافه کنیم:
using System.ComponentModel.DataAnnotations; namespace FluentValidationSample.Models { public class UserModel { [Display(Name = "نام کاربری")] public string Username { get; set; } [Display(Name = "سن")] public int Age { get; set; } [Display(Name = "سابقه کار")] public int Experience { get; set; } [DataType(DataType.EmailAddress)] [Display(Name = "آدرس ایمیل")] public string Email { get; set; } } }
ایجاد سرویسی برای بررسی منحصربفرد بودن آدرس ایمیل
در ادامه قصد داریم سرویسی را ایجاد کنیم که برای مثال با بانک اطلاعاتی ارتباط برقرار کرده (در اینجا جهت سهولت ارائه، از یک آرایه استفاده شدهاست) و مشخص میکند که آیا ایمیل دریافتی پیشتر استفاده شدهاست یا خیر:
using System.Linq; namespace FluentValidationSample.Services { public interface IUsersService { bool IsUniqueEmail(string emailAddress); } public class UsersService : IUsersService { public bool IsUniqueEmail(string emailAddress) { string[] registedEmails = { "email@site.com", "test@gmail.com" }; return !registedEmails.Contains(emailAddress); } } }
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>();
ایجاد اعتبارسنج سمت سرور بررسی منحصربفرد بودن آدرس ایمیل
در ادامه یک PropertyValidator جدید را ایجاد میکنیم تا بتوان توسط آن مقدار ایمیل دریافتی را در سمت سرور، تعیین اعتبار کرد:
using FluentValidation.Validators; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public class UniqueEmailValidator : PropertyValidator { private readonly IUsersService _usersService; public UniqueEmailValidator(IUsersService usersService) : base("این آدرس ایمیل هم اکنون مورد استفادهاست") { _usersService = usersService; } protected override bool IsValid(PropertyValidatorContext context) { return context.PropertyValue != null && _usersService.IsUniqueEmail((string)context.PropertyValue); } } }
پس از آن جهت سهولت استفادهی از آن، یک متد الحاقی جدید را نیز به نام UniqueEmail به نحو زیر تعریف میکنیم:
using FluentValidation; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public static class CustomValidatorExtensions { public static IRuleBuilderOptions<T, string> UniqueEmail<T>( this IRuleBuilder<T, string> ruleBuilder, IUsersService usersService) { return ruleBuilder.SetValidator(new UniqueEmailValidator(usersService)); } } }
یک نکته: اگر دقت کرده باشید، فضای نام این اعتبارسنج در این قسمت FluentValidationSample.ModelsValidations شدهاست:
علت اینجا است که اعتبارسنج تعریف شده نیاز دارد هم از مدلها استفاده کند و هم از سرویس کاربران. سرویس کاربران هم از مدلها استفاده میکند. به همین جهت اگر تعاریف اعتبارسنجی را داخل پروژهی مدلها قرار دهیم، یک وابستگی حلقوی رخ خواهد داد (وابستگی مدلها به سرویسها و برعکس). بنابراین بهتر است اعتبارسنجها را به یک پروژهی مجزا منتقل کنیم تا از بروز این cyclic dependency جلوگیری شود.
اعمال اعتبارسنجی منحصربفرد بودن ایمیل دریافتی به اعتبارسنج UserModel
پس از تهیهی متد الحاقی UniqueEmail، آنرا به RuleFor مخصوص خاصیت ایمیل اضافه میکنیم. در اینجا نیز تزریق وابستگی سرویس سفارشی IUsersService به سازندهی کلاس اعتبارسنج مجاز است:
using FluentValidation; using FluentValidationSample.Models; using FluentValidationSample.Services; namespace FluentValidationSample.ModelsValidations { public class UserModelValidator : AbstractValidator<UserModel> { public UserModelValidator(IUsersService usersService) { RuleFor(x => x.Username).NotNull(); RuleFor(x => x.Age).NotNull(); RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull(); RuleFor(x => x.Email).EmailAddress().NotNull().UniqueEmail(usersService); } } }
ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور
در ادامه نیاز است ویژگیهای data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation; using FluentValidation.AspNetCore; using FluentValidation.Internal; using FluentValidation.Resources; using FluentValidation.Validators; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace FluentValidationSample.ModelsValidations { public class RemoteClientValidator : ClientValidatorBase { public string RemoteUrl { set; get; } public RemoteClientValidator(PropertyRule rule, IPropertyValidator validator) : base(rule, validator) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-remote", GetErrorMessage(context)); MergeAttribute(context.Attributes, "data-val-remote-url", RemoteUrl); } private string GetErrorMessage(ClientModelValidationContext context) { var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName()); string messageTemplate; try { messageTemplate = Validator.Options.ErrorMessageSource.GetString(null); } catch (FluentValidationMessageFormatException) { messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>(); } return formatter.BuildMessage(messageTemplate); } } }
<input dir="ltr" class="form-control input-validation-error" type="email" data-val="true" data-val-email="'آدرس ایمیل' is not a valid email address." data-val-remote="این آدرس ایمیل هم اکنون مورد استفادهاست" data-val-remote-url="/Home/ValidateUniqueEmail" data-val-required="'آدرس ایمیل' must not be empty." id="Email" name="Email" >
افزودن اعتبارسنجهای تعریف شده به تنظیمات برنامه
پس از تعریف UniqueEmailValidator و RemoteClientValidator، روش افزودن آنها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>(); services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()); fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; fv.ConfigureClientsideValidation(clientSideValidation => { // ... clientSideValidation.Add( validatorType: typeof(UniqueEmailValidator), factory: (context, rule, validator) => new RemoteClientValidator(rule, validator) { RemoteUrl = "/Home/ValidateUniqueEmail" }); }); } ); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IUsersService _usersService; public HomeController(IUsersService usersService) { _usersService = usersService; } // ... public IActionResult ValidateUniqueEmail(string email) { return Ok(_usersService.IsUniqueEmail(email)); } } }
تعریف کدهای جاوا اسکریپتی مورد نیاز
پیش از هرکاری، اسکریپتهای فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>
آزمایش برنامه
View این قسمت نیز همانند قسمت قبل است که فقط یک آدرس ایمیل به آن اضافه شدهاست:
برای آزمایش آن اگر برای مثال یکی از آدرسهای ایمیل از پیش تعریف شدهی در متد IsUniqueEmail سرویس کاربران را وارد کنیم، با خطای اعتبارسنجی سمت کلاینت فوق روبرو خواهیم شد.
کدهای کامل این سری را تا این قسمت از اینجا میتوانید دریافت کنید: FluentValidationSample-part04.zip
using Microsoft.Data.Entity; using Microsoft.Data.Entity.Metadata; using System.Collections.Generic; using System.Linq; namespace UsingEF7WithSQLite { public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } } }
PM> Install-Package EntityFramework.SQLite –Pre
namespace UsingEF7SQLiteProvider { public class BloggingContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnConfiguring(DbContextOptions builder) { builder.UseSQLite(@"Data Source=.\BloggingDatabae.db"); } protected override void OnModelCreating(ModelBuilder builder) { builder.Entity<Blog>() .OneToMany(b => b.Posts, p => p.Blog) .ForeignKey(p => p.BlogId); // The EF7 SQLite provider currently doesn't support generated values // so setting the keys to be generated from developer code builder.Entity<Blog>() .Property(b => b.BlogId) .GenerateValueOnAdd(false); builder.Entity<Post>() .Property(b => b.BlogId) .GenerateValueOnAdd(false); } } }
Install-Package EntityFramework.Commands -Pre
Add-Migration MyFirstMigration Apply-Migration
using (var db = new BloggingContext()) { db.Database.AsMigrationsEnabled().ApplyMigrations(); }
using (var db = new Models.BloggingContext()) { db.Blogs.Add(new Models.Blog { Url = "https://www.dntips.ir" }); db.SaveChanges(); foreach (var item in db.Blogs) { Console.WriteLine(item.Url); } } Console.ReadLine();