این قسمت از مقاله به ایده اصلی برنامه نویسی تابعی و دلیل وجودی آن خواهد پرداخت. هیچ شکی نیست که بزرگترین چالش در توسعه نرم افزارهای بزرگ، پیچیدگی آن است. تغییرات همیش اجتناب ناپذیر هستند. به خصوص زمانی که صحبت از پیاده سازی امکان جدیدی باشد، پیچیدگی اضافه خواهد شد. در نتیجه منجر به سخت شدن فهمیدن کد میشود، زمان توسعه را بالاتر میبرد و باگهای ناخواسته را به وجود خواهد آورد. همچنین تغییر هر چیزی در دنیای نرم افزار بدون به وجود آوردن رفتارهای ناخواسته و یا اثرات جانبی، تقریبا غیر ممکن است. در نهایت همه این موارد میتوانند سرعت توسعه را پایین برده و حتی باعث شکست پروژههای نرم افزاری شوند. سبکهای کد نویسی دستوری (Imperative) مانند برنامه نویسی شیء گرا، میتوانند به کاهش این پیچیدگیها تا حد خوبی کمک کنند. البته در صورتیکه به طور صحیحی پیاده شوند. در واقع با ایجاد Abstraction در این مدل برنامه نویسی، پیچیدگیها را مخفی میکنیم.
سیر تکاملی الگوهای برنامه نویسی
برنامه نویسی شیء گرا در خون برنامه نویسهای سی شارپ جاری است؛ ما معمولا ساعتها درباره اینکه چگونه میتوانیم با استفاده از ارث بری و ترتیب پیاده کلاسها، یک هدف خاص برسیم، بر روی کپسوله سازی تمرکز میکنیم و انتزاع (Abstraction) و چند ریختی ( Polymorphism ) را برای تغییر وضعیت برنامه استفاده میکنیم. در این مدل همیشه احتمال این وجود دارد که چند ترد به صورت همزمان به یک ناحیه از حافظه دسترسی داشته باشند و تغییری در آن به وجود بیاورند و باعث به وجود آمدن شرایط Race Condition شوند. البته همگی به خوبی میدانیم که میتوانیم یک برنامهی کاملا Thread-Safe هم داشته باشیم که به خوبی مباحث همزمانی و همروندی را مدیریت کند؛ اما یک مساله اساسی در مورد کارآیی باقی میماند. گرچه Parallelism به ما کمک میکند که کارآیی برنامه خود را افزایش دهیم، اما refactor کردن کدهای موجود، به حالت موازی، کاری سخت و پردردسر خواهد بود.
برنامه نویسی تابعی، یک الگوی برنامه نویسی است که از یک ایده قدیمی (قبل از اولین کامپیوترها !) برگرفته شدهاست؛ زمانیکه دو ریاضیدان، یک تئوری به نام lambda calculus را معرفی کردند، که یک چارچوب محاسباتی میباشد؛ عملیاتی ریاضی را انجام میدهد و نتیجه را محاسبه میکند، بدون اینکه تغییری را در وضعیت دادهها و وضعیت، به وجود بیاورد. با این کار، فهمیدن کدها آسانتر خواهد بود و اثرات جانبی را کمتر خواهد کرد، همچین نوشتن تستها سادهتر خواهند شد.
جالب است اگر زبانهای برنامه نویسی را که از برنامه نویسی تابعی پشتیبانی میکنند، بررسی کنیم، مانند Lisp , Clojure, Erlang, Haskel، هر کدام از این زبانها جنبههای مختلفی از برنامه نویسی تابعی را پوشش میدهند. #F یک عضو از خانواده ML میباشد که بر روی دات نت فریمورک در سال 2002 پیاده سازی شده. ولی جالب است بدانید بیشتر زبانهای همه کاره مانند #C به اندازه کافی انعطاف پذیر هستند تا بتوان الگوهای مختلفی را توسط آنها پیاده کرد. از آنجایی که اکثرا ما از #C برای توسعه نرم افزارهایمان استفاده میکنیم، ترکیب ایدههای برنامه نویسی تابعی میتواند راهکار جالبی برای حل مشکلات ما باشد.
قبلا درباره توابع ریاضی صحت کردیم. در زبانهای برنامه نویسی هم ایده همان است؛ ورودیهای مشخص و خروجی مورد انتظار، بدون تغییری در حالت برنامه. به این مفاهیم شفافیت و صداقت توابع میگوییم که در ادامه با آن بیشتر آشنا میشویم. به این نکته توجه داشته باشید که منظور از تابع در #C فقط Method نیست؛ Func , Action , Delegate هم نوعی تابع هستند.
به طور ساده با نگاه کردن به ورودیهای تابع و نام آنها باید بتوانیم کاری را که انجام میدهد، حدس بزنیم. یعنی یک تابع باید بر اساس ورودیهای آن کاری را انجام دهد و نباید یک پارامتر Global آن را تحت تاثیر قرار دهد. پارامترهای Global میتوانند یک Property در سطح یک کلاس باشند، یا یک شیء که وضعیت آن تحت کنترل تابع نیست؛ مانند شی DateTime. به مثال زیر توجه کنید:
public int CalculateElapsedDays(DateTime from) { DateTime now = DateTime.Now; return (now - from).Days; }
آیا میتوانید این تابع را شفاف کنیم؟ بله!
چطور؟ به سادگی! با تغییر پارامترهای ورودی:
public static int CalculateElapsedDays(DateTime from, DateTime now) => (now - from).Days;
صداقت یک تابع یعنی یک تابع باید همه اطلاعات مربوط به ورودیها و خروجیها را پوشش دهد. به این مثال دقت کنید:
public int Divide(int numerator, int denominator) { return numerator / denominator; }
آیا این همه مواردی را که از آن انتظار داریم پوشش میدهد؟ احتمالا خیر!
اگر دو عدد صحیح را به این تابع بفرستیم، احتمالا مشکلی پیش نخواهد آمد. اما همانطور که حدس میزنید اگر پارامتر دوم 0 باشد چه اتفاقی خواهد افتاد؟
var result = Divide(1,0);
چگونه مشکل را حل کنیم؟ تایپ ورودی را به شکل زیر تغییر دهیم:
public static int Divide(int numerator, NonZeroInt denominator) { return numerator / denominator.Value; }
Functions as first-class values
ترجمه فارسی این کلمه ما را از معنی اصلی آن خیلی دور میکند؛ احتمالا یک ترجمه سادهی آم میتواند «تابع، ارزش اولیه کلاس» باشد!
وقتی توابع first-class values باشند، یعنی میتوانند به عنوان ورودی سایر توابع استفاده شوند، میتوانند به یک متغیر انتساب داده شوند، دقیقا مثل یک مقدار. برای مثال:
Func<int, bool> isMod2 = x => x % 2 == 0; var list = Enumerable.Range(1, 10); var evenNumbers = list.Where(isMod2);
توابع مرتبه بالا! یک یا چند تابع را به عنوان ورودی میگیرند و یک تابع را به عنوان نتیجه بر میگرداند. در مثال بالا Extension Method ، Where یک تابع High-Order میباشد.
پیاده سازی Where احتمالا به شکل زیر میباشد:
public static IEnumerable<T> Where<T>(this IEnumerable<T> ts, Func<T, bool> predicate) { foreach (T t in ts) if (predicate(t)) yield return t; }
2. ملاک تشخیص اینکه چه آیتمهایی در لیست باید وجود داشته باشند، به عهده متدی میباشد که آن را فراخوانی میکند.
در این مثال، تابع Where، تابع ورودی را به ازای هر المان، در لیست فراخوانی میکند. این تابع میتواند طوری طراحی شود که تابع ورودی را به صورت شرطی اعمال کند. آزمایش این حالت به عهده شما میباشد. اما به صورت کلی انتظار میرود که قدرت توابع High-Order را درک کرده باشید.
الف) اگر از Web forms استفاده میکنید: استفاده از Script manager (^ و ^)
ب) اگر از MVC استفاده میکنید: استفاده از Bundling & minification
در هر دو حالت نحوه ارائه اسکریپتها تحت کنترل برنامه ASP.NET در خواهد آمد و مستقیما و بدون دخالت ASP.NET، توسط IIS توزیع نمیشوند.
- برای مپ کردن فایلهای استاتیک به موتور ASP.NET میشود از StaticFileHandler استفاده کرد. اگر کش کردن اطلاعات استاتیک در سمت سرور فعال شود، این مساله بار اضافهای را به سرور تحمیل نخواهد کرد.
<system.web> <httpHandlers> <add path="*.js" verb="*" type="System.Web.StaticFileHandler" /> </httpHandlers>
برای این کار از دادههای موجود در پایگاه داده [AdventureWorksLT2008R2].[SalesLT].[Address] استفاده میکنم .
نمونه خروجی مانند زیر میباشد :
سپس برای تنظیمات Script Component به ترتیب زیر عمل میکنیم :
در قسمت Input column ستون هایی را که به عنوان پارامتر میتوانیم با آنها کار کنیم ، تعریف میکنیم : (دقت شود که تغییر نام متغیرها ، در کدها اعمال میشوند .)
حال میخواهم ستونهای تاریخ میلادی و شمسی را برای خروجی تعریف کنم :
همانطور که مشاهده میکنید ، نوع داده ای برای خروجی را رشته تعریف کردم.
سپس به قسمت script بر میگردیم (سمت چپ پنجره ) و روی Edit Script کلیک میکنیم : (در صورتی که تمایل به کد نویسی با VB را دارید در همین قسمت میتوانید آن را تنظیم کنید)
پنجره ای مانند زیر برای ویرایش کدها ، باز میشود
همانطور که مشاهده میکنید درون این کلاس 4 متد تبدیل تاریخ را پیاده کردم و از آنها در متد input0_processInputRow استفاده کردم . این کار نیازی به پیاده سازی حلقه ندارد و به راحتی میتوان آنها را روی سطرهای دلخواه تعریف کرد . خروجی نمایش داده شده در data viewerها مانند زیر میباشند :
قبل از اعمال تبدیلات :
بعد از اعمال تبدیلات :
- Lead Function:
LEAD ( scalar_expression [ ,offset ] , [ default ] ) OVER ( [ partition_by_clause ] order_by_clause )
- Scalar_expression: در Scalar_expression، نام یک فیلد یا ستون درج میشود، و مقدار برگشتی فیلد مورد نظر، به مقدار تعیین شده offset نیز بستگی دارد. خروجی Scalar_expression فقط یک مقدار است.
- offset: منظور از Offset در این Syntax همانند عملکرد Offset در Syntax مربوط به Over میباشد. یعنی هر عددی برای offset در نظر گرفته شود، بیانگر نقطه آغازین سطر بعدی یا قبلی نسبت به سطر جاری است. به بیان دیگر، عدد تعیین شده در Offset به Sql server میفهماند چه تعداد سطر را در محاسبه در نظر نگیرد.
- Default: زمانی که برای Offset مقداری را تعیین مینمایید، SQL Server به تعداد تعیین شده در Offset، سطرها را در نظر نمیگیرد، بنابراین مقدار خروجی Scalar_expression بطور پیش فرض Null در نظر گرفته میشود، چنانچه بخواهید، مقداری غیر از Null درج نمایید، میتوانید مقدار دلخواه را در قسمت Default وارد کنید.
- (OVER ( [ partition_by_clause ] order_by_clause : در بخش اول بطور کامل توضیح داده شده است.
Create Table TestLead_LAG (SalesOrderID int not null, SalesOrderDetailID int not null , OrderQty smallint not null); GO Insert Into TestLead_LAG Values (43662,49,1),(43662,50,3),(43662,51,1), (43663,52,1),(43664,53,1),(43664,54,1), (43667,77,3),(43667,78,1),(43667,79,1), (43667,80,1),(43668,81,3),(43669,110,1), (43670,111,1),(43670,112,2),(43670,113,2), (43670,114,1),(43671,115,1),(43671,116,2)
مثال:قصد داریم در هر سطر مقدار بعدی فیلد SalesOrderDetailID در فیلد دیگری به نام LeadValue نمایش دهیم، بنابراین Script زیر را ایجاد میکنیم:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LEAD(SalesOrderDetailID) OVER (ORDER BY SalesOrderDetailID) LeadValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQtyخروجی بصورت زیر خواهد بود:
مطابق شکل، براحتی واضح است، که در هر سطر مقدار بعدی فیلد SalesOrderDetailID در فیلد LeadValue درج و نمایش داده میشود. فقط در سطر 10، چون مقدار بعدی برای فیلد SalesOrderDetailID وجود ندارد، SQL Server مقدار فیلد LeadValue را، Null در نظر میگیرد.در این مثال فقط از آرگومان Scalar_expression، استفاده کردیم، و Offset و Default را مقدار دهی ننمودیم، بنابراین SQL Server بطور پیش فرض هیچ سطری را حذف نمیکند و مقدار Default را Null در نظر میگیرد.مثال دوم: قصد داریم در هر سطر مقدار دو سطر بعدی فیلد SalesOrderDetailID را در فیلد LeadValue نمایش دهیم، و در صورت وجود نداشتن مقدار فیلد SalesOrderDetailID، مقدار پیش فرض صفر ،در فیلد LeadValue قرار دهیم،بنابراین Script آن بصورت زیر خواهد شد:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LEAD(SalesOrderDetailID,2,0) OVER (ORDER BY SalesOrderDetailID) LeadValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQtyخروجی:
در صورت مسئله بیان کرده بودیم، در هر سطر،مقدار فیلد SalesOrderDetailID دو سطر بعدی، را نمایش دهیم، بنابراین مقداری که برای Offset در نظر میگیریم، برابر دو خواهد بود، سپس گفته بودیم، چنانچه در هر سطر مقدار فیلد SalesOrderDetailID وجود نداشت،بجای مقدار پیش فرض Null،از مقدار صفر استفاده شود، بنابراین به Default مقدار صفر را نسبت دادیم.LEAD(SalesOrderDetailID,2,0)در شکل، مطابق صورت مسئله، مقدار فیلد LeadValue سطر اول برابر است با 78،به بیان سادهتر برای بدست آوردن مقدار فیلد LaedValue هر سطر، میبایست هر سطر را به علاوه 2 (Offset) نماییم، تا سطر بعدی بدست آید، سپس مقدار SalesOrderDetailID را در فیلد LeadValue قرار میدهیم.به سطر 9 و 10 توجه نمایید، که مقدار فیلد LeadValue آنها برابر با صفر است، واضح است، سطر 10 + 2 برابر است با 12( 10+2=12 )، چنین سطری در خروجی نداریم، بنابراین بطور پیش فرض مقدار LeadVaule توسط Sql Server برابر Null در نظر گرفته میشود، اما نمیخواستیم، که این مقدار Null باشد، بنابراین به آرگومان Default مقدار صفر را نسبت دادیم، تا SQL Server ، به جای استفاده از Null، مقدار در نظر گرفته شده صفر را استفاده نماید.اگر چنین فانکشنی وجود نداشت، برای شبیه سازی آن میبایست از Join روی خود جدول استفاده مینمودیم، و یکسری محاسابت دیگر، که کار را سخت مینمود، مثال دوم را با Script زیر میتوان شبیه سازی نمود:WITH cteLead AS ( SELECT SalesOrderID,SalesOrderDetailID,OrderQty, ROW_NUMBER() OVER (ORDER BY SalesOrderDetailID) AS sn FROM TestLead_LAG WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ) SELECT m.SalesOrderID, m.SalesOrderDetailID, m.OrderQty, case when sLead.SalesOrderDetailID is null Then 0 Else sLead.SalesOrderDetailID END as leadvalue FROM cteLead AS m LEFT OUTER JOIN cteLead AS sLead ON sLead.sn = m.sn+2 ORDER BY m.SalesOrderID, m.SalesOrderDetailID, m.OrderQtyجدول موقتی ایجاد نمودیم، که ROW_Number را در آن اضافه کردیم، سپس جدول ایجاد شده را با خود Join کردیم، و گفتیم، که مقدار فیلدLeadValue هر سطر برابر است با مقدار فیلد SalesOrderDetailID دو سطر بعد از آن. و با Case نیز مقدار پیش فرض را صفر در نظر گرفتیم.
- LAG Function:
این فانکشن نیز در SQL Server 2012 ارائه شده است، و امکان دسترسی، به Dataهای سطر قبلی نسبت به سطر جاری را در نتیجه یک پرس و جو (Query)، ارائه میدهد. بدون آنکه از Self-join استفاده نمایید،Syntax آن شبیه به فانکشن Lead میباشد و بصورت زیر است:LAG (scalar_expression [,offset] [,default]) OVER ( [ partition_by_clause ] order_by_clause )Syntax مربوط به فانکشن LAG را شرح نمیدهم، بدلیل آنکه شبیه به فانکشن Lead میباشد، فقط تفاوت آن در Offset است، Offset در فانکشن LAG روی سطرهای ماقبل سطر جاری اعمال میگردد.مثال دوم را برای حالت LAG Function شبیه سازی مینماییم:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LAG(SalesOrderDetailID,2,0) OVER (ORDER BY SalesOrderDetailID) LAGValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty goخروجی :
همانطور که گفتیم، LAG Function عکس LEAD Function میباشد. یعنی مقدار فیلد LAGValue سطر جاری برابر است با مقدار SalesOrderDetailID دو سطر ما قبل خود.مقدار فیلد LAGValue دو سطر اول و دوم نیز برابر صفر است، چون دو سطر ماقبل آنها وجود ندارد، و مقدار صفر نیز بدلیل این است که Default را برابر صفر در نظر گرفته بودیم.مثال: در این مثال از Laed Function و LAG Function بطور همزمان استفاده میکنیم، با این تفاوت، که از گروه بندی نیز استفاده شده است:Script زیر را اجرا نمایید:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, Lead(SalesOrderDetailID) OVER (PARTITION BY SalesOrderID ORDER BY SalesOrderDetailID) LeadValue, LAG(SalesOrderDetailID) OVER (PARTITION BY SalesOrderID ORDER BY SalesOrderDetailID) LAGValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty goخروجی:با بررسی هایی که در مثالهای قبل نمودیم،خروجی زیر را میتوان براحتی تشخیص داد، و توضیح بیشتری نمیدهم.موفق باشید.
using System; using System.ComponentModel.DataAnnotations; namespace AngularTemplateDrivenFormsLab.Models { public class Movie { public int Id { get; set; } [Required(ErrorMessage = "Movie Title is Required")] [MinLength(3, ErrorMessage = "Movie Title must be at least 3 characters")] public string Title { get; set; } [Required(ErrorMessage = "Movie Director is Required.")] public string Director { get; set; } [Range(0, 100, ErrorMessage = "Ticket price must be between 0 and 100.")] public decimal TicketPrice { get; set; } [Required(ErrorMessage = "Movie Release Date is required")] public DateTime ReleaseDate { get; set; } } }
using AngularTemplateDrivenFormsLab.Models; using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class MoviesController : Controller { [HttpPost] public IActionResult Post([FromBody]Movie movie) { if (ModelState.IsValid) { // TODO: save ... return Ok(movie); } ModelState.AddModelError("", "This record already exists."); // a cross field validation return BadRequest(ModelState); } } }
الف) خطاهای اعتبارسنجی در سطح فیلدها
زمانیکه return BadRequest(ModelState) صورت میگیرد، محتویات شیء ModelState به همراه status code مساوی 400 به سمت کلاینت ارسال خواهد شد. در شیء ModelState یک دیکشنری که کلیدهای آن، نام خواص و مقادیر متناظر با آنها، خطاهای اعتبارسنجی تنظیم شدهی در مدل است، قرار دارند.
ب) خطاهای اعتبارسنجی عمومی
در این بین میتوان دیکشنری ModelState را توسط متد AddModelError نیز تغییر داد و برای مثال کلید آنرا مساوی "" تعریف کرد. در این حالت یک چنین خطایی به کل فرم اشاره میکند و نه به یک خاصیت خاص.
نمونهای از خروجی نهایی ارسالی به سمت کاربر:
{"":["This record already exists."],"TicketPrice":["Ticket price must be between 0 and 100."]}
به همین جهت نیاز است بتوان خطاهای حالت (الف) را دقیقا در ذیل هر فیلد و خطاهای حالت (ب) را در بالای فرم به صورت عمومی به کاربر نمایش داد:
پردازش و دریافت خطاهای اعتبارسنجی سمت سرور در یک برنامهی Angular
با توجه به اینکه سرور، شیء ModelState را توسط return BadRequest به سمت کلاینت ارسال میکند، برای پردازش دیکشنری دریافتی از سمت آن، تنها کافی است قسمت بروز خطای عملیات ارسال اطلاعات را بررسی کنیم:
در این HttpErrorResponse دریافتی، دو خاصیت error که همان آرایهی دیکشنری نام خواص و پیامهای خطای مرتبط با هر کدام و status code دریافتی مهم هستند:
errors: string[] = []; processModelStateErrors(form: NgForm, responseError: HttpErrorResponse) { if (responseError.status === 400) { const modelStateErrors = responseError.error; for (const fieldName in modelStateErrors) { if (modelStateErrors.hasOwnProperty(fieldName)) { const modelStateError = modelStateErrors[fieldName]; const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)]; if (control) { // integrate into Angular's validation control.setErrors({ modelStateError: { error: modelStateError } }); } else { // for cross field validations -> show the validation error at the top of the screen this.errors.push(modelStateError); } } } } else { this.errors.push("something went wrong!"); } } lowerCaseFirstLetter(data: string): string { return data.charAt(0).toLowerCase() + data.slice(1); }
در اینجا از آرایهی errors برای نمایش خطاهای عمومی در سطح فرم استفاده میکنیم. این خطاها در ModelState، دارای کلید مساوی "" هستند. به همین جهت حلقهای را بر روی شیء responseError.error تشکیل میدهیم. به این ترتیب میتوان به نام خواص و همچنین خطاهای متناظر با آنها رسید.
const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
در ادامه اگر control ایی یافت شد، توسط متد setErrors، کلید جدید modelStateError را که دارای خاصیت سفارشی error است، تنظیم میکنیم. با اینکار سبب خواهیم شد تا خطای اعتبارسنجی دریافتی از سمت سرور، با سیستم اعتبارسنجی Angular یکی شود. به این ترتیب میتوان این خطا را دقیقا ذیل همین کنترل در فرم نمایش داد. اگر کنترلی یافت نشد (کلید آن "" بود و یا جزو نام کنترلهای موجود در آرایهی form.controls نبود)، این خطا را به آرایهی errors اضافه میکنیم تا در بالاترین سطح فرم قابل نمایش شود.
نحوهی استفادهی از متد processModelStateErrors فوق را در متد submitForm، در قسمت شکست عملیات ارسال اطلاعات، مشاهده میکنید:
model = new Movie("", "", 0, ""); successfulSave: boolean; errors: string[] = []; constructor(private movieService: MovieService) { } ngOnInit() { } submitForm(form: NgForm) { console.log(form); this.errors = []; this.movieService.postMovieForm(this.model).subscribe( (data: Movie) => { console.log("Saved data", data); this.successfulSave = true; }, (responseError: HttpErrorResponse) => { this.successfulSave = false; console.log("Response Error", responseError); this.processModelStateErrors(form, responseError); }); }
نمایش خطاهای اعتبارسنجی عمومی فرم
اکنون که کار مقدار دهی آرایهی errors انجام شدهاست، میتوان حلقهای را بر روی آن تشکیل داد و عناصر آنرا در بالای فرم، به صورت عمومی و مستقل از تمام فیلدهای آن نمایش داد:
<form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="alert alert-danger" role="alert" *ngIf="errors.length > 0"> <ul> <li *ngFor="let error of errors"> {{ error }} </li> </ul> </div> <div class="alert alert-success" role="alert" *ngIf="successfulSave"> Movie saved successfully! </div>
نمایش خطاهای اعتبارسنجی در سطح فیلدهای فرم
با توجه به تنظیم خطاهای اعتبارسنجی کنترلهای Angular در متد processModelStateErrors و داشتن کلید جدید modelStateError
control.setErrors({ modelStateError: { error: modelStateError } });
<ng-template #validationErrorsTemplate let-ctrl="control"> <div *ngIf="ctrl.invalid && ctrl.touched"> <div class="alert alert-danger" *ngIf="ctrl.errors.required"> This field is required. </div> <div class="alert alert-danger" *ngIf="ctrl.errors.minlength"> This field should be minimum {{ctrl.errors.minlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="ctrl.errors.maxlength"> This field should be max {{ctrl.errors.maxlength.requiredLength}} characters. </div> <div class="alert alert-danger" *ngIf="ctrl.errors.pattern"> This field's pattern: {{ctrl.errors.pattern.requiredPattern}} </div> <div class="alert alert-danger" *ngIf="ctrl.errors.modelStateError"> {{ctrl.errors.modelStateError.error}} </div> </div> </ng-template>
<div class="form-group" [class.has-error]="releaseDate.invalid && releaseDate.touched"> <label class="control-label" for="releaseDate">Release Date</label> <input type="text" name="releaseDate" #releaseDate="ngModel" class="form-control" required [(ngModel)]="model.releaseDate" /> <ng-container *ngTemplateOutlet="validationErrorsTemplate; context:{ control: releaseDate }"></ng-container> </div>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
در ابتدای بحث، برای آشنایی بیشتر با HTML Helperها به مطالعه این مقاله بپردازین.
در این مقاله قرار است برای یک HTML Helper خاص، قالب نمایشی اختصاصی خودمان را طراحی کنیم و به نحوی HTML Helper موجود را سفارشی سازی کنیم. به عنوان مثال میخواهیم خروجی یک EditorFor() برای یک نوع خاص، به حالت دلخواهی باشد که ما خودمان آن را تولیدش کردیم؛ یا اصلا نه. حتی میشود برای خروجی یک EditorFor() که خصوصیتی از جنس string را میخواهیم به آن انتساب دهیم، به جای تولید input، یک مقدار متنی را برگردانیم. به این حالت:
<div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div>
در ادامه یک پروژهی عملی را شروع کرده و در آن کاری را که میخواهیم، انجام میدهیم. پروژهی ما به این شکل میباشد که قرار است در آن به ثبت کتاب بپردازیم و برای هر کتاب هم یک سبک داریم و قسمت سبک کتابهای ما یک Enum است که از قبل میخواهیم مقدارهایش را تعریف کنیم.
مدل برنامه
public class Books { public int Id { get; set; } [Required] [StringLength(255)] public string Name { get; set; } public Genre Genre { get; set; } }
public enum Genre { [Display(Name = "Non Fiction")] NonFiction, Romance, Action, [Display(Name = "Science Fiction")] ScienceFiction }
در داخل کلاس Books یک خصوصیت از جنس Genre برای سبک کتابها داریم و در داخل نوع شمارشی Genre، سبکهای ما تعریف شدهاند. همچنین هر کدام از سبکها هم به ویژگی Display مزین شدهاند تا بتونیم بعدا از مقدار آنها استفاده کنیم.
کنترلر برنامه
public class BookController : Controller { // GET: Book public ActionResult Index() { return View(DataAccess.DataContext.Book.ToList()); } public ActionResult Create() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.Add(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Edit(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.AddOrUpdate(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Details(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } }
در قسمت کنترلر هم کار خاصی جز عملیات اصلی نوشته نشدهاست. لیست کتابها را از پایگاه داده بیرون آوردیم و از طریق اکشن Index به نمایش گذاشتیم. با اکشنهای Create، Edit و Details هم کارهای روتین مربوط به خودشان را انجام دادیم. نکتهی قابل تذکر، DataAccess میباشد که کلاسی است که با آن ارتباط برقرار شده با EF و سپس اطلاعات واکشی و تزریق میشوند.
View مربوط به اکشن Create برنامه
@using Book.Entities @model Book.Entities.Books @{ ViewBag.Title = "Create"; } <h2>New Book</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <h4>Books</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="Create" /> <input type="reset" value="Reset" /> @Html.ActionLink("Back to List", "Index", null, new {@class="btn btn-default"}) </div> </div> </div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
View برنامه هم همان ویویی است که خود Visual Studio برای ما ساختهاست. به جز یک سری دستکاریهایی داخل سیاساس، هدف از گذاشتن View مربوط به Create این بود که قرار است بر روی این قسمت کار کنیم. اگر پروژه رو اجرا کنید و به قسمت Create بروید، مشاهده خواهید کرد که برای Genre یک input ساخته شدهاست که کاربر باید در آن مقدار وارد کند. ولی اگر یادتان باشد، ما سبکهای نگارشی خودمان را در نوع شمارشی Genre ایجاد کرده بودیم. پس عملا باید یک لیست به کاربر نشان داده شود که تا از آن لیست، نوع را انتخاب کند. میتوانیم بیایم همینجا در داخل View مربوطه، بهجای استفاده از HTML Helper پیشفرض، از DropDownList یا EnumFor استفاده کنیم و به طریقی این لیست را ایجاد کنیم. ولی چون قرار است در این مثال به شرح موضوع مقاله خودمان بپردازیم، این کار را انجام نمیدهیم.
در حقیقیت میخوایم متد EditorFor را طوری سفارشی سازی کنیم که برای نوع شمارشی Genre، به صورت خودکار یک لیست ایجاد کرده و برگرداند. از نسخهی سوم ASP.NET MVC به بعد این امکان برای توسعه دهندهها فراهم شدهاست. شما میتوانید در پوشهی Shared داخل پوشه Views برنامه، پوشهای را به اسم EditorTemplates ایجاد کنید؛ همینطور DisplayTemplates و برای نوع خاصی که میخواهید سفارشیسازی را برای آن انجام دهید، یک PartialView بسازید.
Views/Shared/DisplayTemplates/<type>.cshtml
کاری که الان میخواهیم انجام دهیم این است که یک SelectListItem ایجاد کرده تا مقدارهای نوع Genreمان داخلش باشد و بتوانیم به راحتی برای ساختن DropDownList از آن استفاده کنیم. برای این کار Helper مخصوص خودمان را ایجاد میکنیم. پوشهای به اسم Helpers در کنار پوشههای Controllers، Models ایجاد میکنیم و در داخل آن کلاسی به اسم EnumHelpers میسازیم.
public static class EnumHelpers { public static IEnumerable<SelectListItem> GetItems( this Type enumType, int? selectedValue) { if (!typeof(Enum).IsAssignableFrom(enumType)) { throw new ArgumentException("Type must be an enum"); } var names = Enum.GetNames(enumType); var values = Enum.GetValues(enumType).Cast<int>(); var items = names.Zip(values, (name, value) => new SelectListItem { Text = GetName(enumType, name), Value = value.ToString(), Selected = value == selectedValue } ); return items; } static string GetName(Type enumType, string name) { var result = name; var attribute = enumType .GetField(name) .GetCustomAttributes(inherit: false) .OfType<DisplayAttribute>() .FirstOrDefault(); if (attribute != null) { result = attribute.GetName(); } return result; } }
در توضیح کد بالا عنوان کرد که متدها بهصورت متدهای الحاقی به نوع Type نوشته شدند. کار خاصی در بدنهی متدها انجام نشدهاست. در بدنهی متد اول لیست آیتمها را تولید کردیم. در هنگام ساخت SelectListItem برای گرفتن Text، متد GetName را صدا زدیم. برای اینکه بتوانیم مقدار ویژگی Display که در هنگام تعریف نوع شمارشی استفاده کردیم را بدست بیاریم، باید چک کنیم ببینیم که آیا این آیتم به این ویژگی مزین شدهاست یا نه. اگر شده بود مقدار را میگیریم و به خصوصیت Text متد اول انتساب میدهیم.
@using Book.Entities @using Book.Web.Helpers @{ var items = typeof(Genre).GetItems((int?)Model); } @Html.DropDownList("", items, new {@class="form-control"})
کدهایی که در بالا مشاهده میکنید کدهایی میباشند که قرار است داخل PartialViewی Genre قرار دهیم که در پوشهی EditorTemplates ساختیم. ابتدا آمدیم آیتمها را گرفتیم و بعد به DropDownList دادیم تا لیست نوع را برای ما بسازد. حالا اگه برنامه را اجرا کنید میبینید که EditorFor برای شما یه لیست از نوع شمارشی ساخته و حالا قابل استفاده هست.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
- فرض کنید یک class library مخصوص NET Core. را به نام Core1RtmTestResources.ExternalResources جهت درج منابع تهیه کردهاید و پوشهی Resources را از پروژهی اصلی به آن انتقال دادهاید (بدون هیچ تغییر نامی).
- نیازی نیست تا قسمت options.ResourcesPath کلاس آغازین برنامه را تغییر دهید و همان مقدار Resources در این حالت هم کار میکند.
- فقط نکتهی مهم اینجا است:
public TestLocalController( IStringLocalizer<TestLocalController> stringLocalizer, IHtmlLocalizer<TestLocalController> htmlLocalizer)
الان چون این اسمبلی دیگر اسمبلی جاری نیست، باید به نحو زیر عمل کرد:
private readonly IStringLocalizer _stringLocalizer; private readonly IHtmlLocalizer _htmlLocalizer; public TestLocalController( IStringLocalizerFactory stringLocalizerFactory, IHtmlLocalizerFactory htmlLocalizerFactory) { _stringLocalizer = stringLocalizerFactory.Create( baseName: "Controllers.TestLocalController" /*مشخصات کنترلر جاری*/, location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/); _htmlLocalizer = htmlLocalizerFactory.Create( baseName: "Controllers.TestLocalController" /*مشخصات کنترلر جاری*/, location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/); }
ASP.NET Core 3x دیگر به صورت پیشفرض به همراه Json.NET ارائه نمیشود
در برنامههای ASP.NET Core 3x، وابستگی ثالث Json.NET حذف شدهاست و از این پس هر نوع خروجی JSON آن، مانند بازگشت مقادیر مختلف از اکشن متدهای کنترلرها، به صورت خودکار در پشت صحنه از امکانات ارائه شدهی در System.Text.Json استفاده میکند و دیگر Json.NET، کتابخانهی پیشفرض کار با JSON آن نیست. بنابراین برای کار با آن نیاز به تنظیم خاصی نیست. همینقدر که یک پروژهی جدید ASP.NET Core 3x را ایجاد کنید، یعنی در حال استفادهی از System.Text.Json هستید.
روش بازگشت به Json.NET در ASP.NET Core 3x
اگر به هر دلیلی هنوز نیاز به استفادهی از کتابخانهی Json.NET را دارید، آداپتور ویژهی آن نیز تدارک دیده شدهاست. برای اینکار:
الف) ابتدا باید بستهی نیوگت Microsoft.AspNetCore.Mvc.NewtonsoftJson را نصب کنید.
ب) سپس در کلاس Startup، باید این کتابخانه را به صورت یک سرویس جدید، با فراخوانی متد AddNewtonsoftJson، معرفی کرد:
public void ConfigureServices(IServiceCollection services) { services.AddControllers() .AddNewtonsoftJson() // ... }
روش کار مستقیم با System.Text.Json
اگر در قسمتی از برنامهی خود نیاز به کار مستقیم با اشیاء JSON را داشته باشید و یا حتی بخواهید از این قابلیت در برنامههای کنسول و یا کتابخانهها نیز استفاده کنید، روش انتقال کدهایی که از Json.NET استفاده میکنند به System.Text.Json، به صورت زیر است:
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public DateTime? BirthDay { get; set; } }
using System; using System.Text.Json.Serialization; namespace ConsoleApp { class Program { static void Main(string[] args) { Person person = JsonSerializer.Parse<Person>(...); string json = JsonSerializer.ToString(person); } } }
کلاس JsonSerializer دارای overloadهای زیر برای کار با متدهای Parse و ToString است:
namespace System.Text.Json.Serialization { public static class JsonSerializer { public static object Parse(ReadOnlySpan<byte> utf8Json, Type returnType, JsonSerializerOptions options = null); public static object Parse(string json, Type returnType, JsonSerializerOptions options = null); public static TValue Parse<TValue>(ReadOnlySpan<byte> utf8Json, JsonSerializerOptions options = null); public static TValue Parse<TValue>(string json, JsonSerializerOptions options = null); public static string ToString(object value, Type type, JsonSerializerOptions options = null); public static string ToString<TValue>(TValue value, JsonSerializerOptions options = null); } }
سفارشی سازی JsonSerializer جدید
اگر به امضای متدهای Parse و ToString کلاس JsonSerializer دقت کنید، دارای یک پارامتر اختیاری از نوع JsonSerializerOptions نیز هستند که به صورت زیر تعریف شدهاست:
public sealed class JsonSerializerOptions { public bool AllowTrailingCommas { get; set; } public int DefaultBufferSize { get; set; } public JsonNamingPolicy DictionaryKeyPolicy { get; set; } public bool IgnoreNullValues { get; set; } public bool IgnoreReadOnlyProperties { get; set; } public int MaxDepth { get; set; } public bool PropertyNameCaseInsensitive { get; set; } public JsonNamingPolicy PropertyNamingPolicy { get; set; } public JsonCommentHandling ReadCommentHandling { get; set; } public bool WriteIndented { get; set; } }
// Json.NET: var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; string json = JsonConvert.SerializeObject(person, settings);
// JsonSerializer: var options = new JsonSerializerOptions { IgnoreNullValues = true }; string json = JsonSerializer.ToString(person, options);
در برنامههای ASP.NET Core که این نوع متدها در پشت صحنه فراخوانی میشوند، روش تنظیم JsonSerializerOptions به صورت زیر است:
services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
نگاشت نام ویژهی خواص در حین عملیات deserialization
در مثال فوق، فرض شدهاست که نام خاصیت BirthDay، دقیقا با اطلاعاتی که از رشتهی JSON دریافتی پردازش میشود، تطابق دارد. اگر این نام در اطلاعات دریافتی متفاوت است، میتوان از ویژگی JsonPropertyName برای تعریف این نگاشت استفاده کرد:
[JsonPropertyName("birthdate")] public DateTime? BirthDay { get; set; }
var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; string json = JsonSerializer.ToString(person, options);
در این بین اگر نمیخواهید خاصیتی در عملیات serialization و یا برعکس آن پردازش شود، میتوان از تعریف ویژگی [JsonIgnore] بر روی آن استفاده کرد.
WebStorage: قسمت دوم
window.localStorage window.sessionStorage
if(typeof(Storage) !== "undefined") { // Code for localStorage/sessionStorage. } else { // Sorry! No Web Storage support.. }
localStorage.setItem("lastname", "Smith"); //======================== localStorage.getItem("lastname");
var a=localStorage.lastname;
//ذخیره مقدار store.set('username', 'marcus') //بازیابی مقدار store.get('username') //حذف مقدار store.remove('username') //حذف تمامی مقادیر ذخیره شده store.clear() //ذخیره ساختار store.set('user', { name: 'marcus', likes: 'javascript' }) //بازیابی ساختار به شکل قبلی var user = store.get('user') alert(user.name + ' likes ' + user.likes) //تغییر مستقیم مقدار قبلی store.getAll().user.name == 'marcus' //بازخوانی تمام مقادیر ذخیر شده توسط یک حلقه store.forEach(function(key, val) { console.log(key, '==', val) })
<script src="store.min.js"></script> <script> init() function init() { if (!store.enabled) { alert('Local storage is not supported by your browser. Please disable "Private Mode", or upgrade to a modern browser.') return } var user = store.get('user') // ... and so on ... } </script>
var storeWithExpiration = { // دریافت کلید و مقدار و زمان انقضا به میلی ثانیه set: function(key, val, exp) { //ایجاد زمان فعلی جهت ثبت تاریخ ایجاد store.set(key, { val:val, exp:exp, time:new Date().getTime() }) }, get: function(key) { var info = store.get(key) //در صورتی که کلید داده شده مقداری نداشته باشد نال را بر میگردانیم if (!info) { return null } //تاریخ فعلی را منهای تاریخ ثبت شده کرده و در صورتی که //از مقدار میلی ثاینه بیشتر باشد یعنی منقضی شده و نال بر میگرداند if (new Date().getTime() - info.time > info.exp) { return null } return info.val } } // استفاده عملی از کد بالا // استفاده از تایمر جهت نمایش واکشی دادهها قبل از نقضا و بعد از انقضا storeWithExpiration.set('foo', 'bar', 1000) setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 500) // -> "bar" setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 1500) // -> null
CrossStorageHub.init([ {origin: /\.example.com$/, allow: ['get']}, {origin: /:\/\/(www\.)?example.com$/, allow: ['get', 'set', 'del']} ]);
valid.example.com
invalid.example.com.malicious.com
{ 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE', 'Access-Control-Allow-Headers': 'X-Requested-With', 'Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-Content-Security-Policy': "default-src 'unsafe-inline' *", 'X-WebKit-CSP': "default-src 'unsafe-inline' *", }
<script type="text/javascript" src="~/Scripts/cross-storage/hub.js"></script> <script> CrossStorageHub.init([ {origin: /.*localhost:300\d$/, allow: ['get', 'set', 'del']} ]); </script>
var storage = new CrossStorageClient('http://localhost:3000/example/hub.html'); var setKeys = function () { return storage.set('key1', 'foo').then(function() { return storage.set('key2', 'bar'); }); };
storage.onConnect().then(function() { return storage.set('key', {foo: 'bar'}); }).then(function() { return storage.set('expiringKey', 'foobar', 10000); });
storage.onConnect().then(function() { return storage.get('key1'); }).then(function(res) { return storage.get('key1', 'key2', 'key3'); }).then(function(res) { // ... });
storage.onConnect() .then(function() { return storage.get('key1', 'key2'); }) .then(function(res) { console.log(res); // ['foo', 'bar'] })['catch'](function(err) { console.log(err); });
<script src="https://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js"></script>
var request = indexedDB.open("library"); request.onupgradeneeded = function() { // The database did not previously exist, so create object stores and indexes. var db = request.result; var store = db.createObjectStore("books", {keyPath: "isbn"}); var titleIndex = store.createIndex("by_title", "title", {unique: true}); var authorIndex = store.createIndex("by_author", "author"); // Populate with initial data. store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); }; request.onsuccess = function() { db = request.result; };
var tx = db.transaction("books", "readwrite"); var store = tx.objectStore("books"); store.put({title: "Quarry Memories", author: "Fred", isbn: 123456}); store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567}); store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678}); tx.oncomplete = function() { // All requests have succeeded and the transaction has committed. };
var tx = db.transaction("books", "readonly"); var store = tx.objectStore("books"); var index = store.index("by_author"); var request = index.openCursor(IDBKeyRange.only("Fred")); request.onsuccess = function() { var cursor = request.result; if (cursor) { // Called for each matching record. report(cursor.value.isbn, cursor.value.title, cursor.value.author); cursor.continue(); } else { // No more matching records. report(null); } };