Blazor 5x - قسمت 19 - کار با فرمها - بخش 7 - نکات ویژهی کار با EF-Core در برنامههای Blazor Server
// Do NOT do this anymore //services.AddDbContext(o => o.UseSQlite("filename.db")); // But instead do this: services.AddDbContextFactory(o => o.UseSQlite("filename.db"));
public class MyService { private readonly IDbContextFactorey _contextFactory; public MyService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } // create a new context for each operation / unit of work public async Task DoSomethingAsync() { using (var ctx = _contextFactory.CreateDbContext()) { // this using clause contains your unit of work } } }
اجرای این نوع صفحات کار سختی نیست؛ با کمی جستجو در اینترنت مثلا در اینجا میتوانید چیزهای خوبی پیدا کنید. اما متاسفانه اکثر مثالها چیزی شبیه قرار دادن پارشال "ورود اعضا" در کنار پارشال "ثبت نام" هستند. حتما متوجه شدهاید که معمولا این دو صفحه پس از PostBack به صفحهای جدید هدایت میشوند و یا در بهترین حالت به کمک Ajax ، پس از انجام عملیات، پیامی به کاربر نمایش میدهیم.
در این مقاله سعی شده روشی برای ایجاد چند فرم در یک View توضیح داده شود با این شرط که:
اولا : از Ajax یا هلپر ایجکسی استفاده نکنیم.
ثانیا : پس از post-back، عملیات Redirect را انجام ندهیم و صفحه جاری را حفظ کنیم؛ چه قرار باشد همه چیز درست انجام شده باشد و چه مشکلی پیش آمده باشد و پیام خطایی در کنار فیلدها نمایش داده شود.
در این روش به این نکته توجه شده که هر مدل پس از Post-back حفظ شود و مستقل از دیگری رفتار کند. مثلا
اگر یکی از فرمها ناقص پر شد و دکمهی ارسال آن فشرده شد، پس از Post-back، فقط و فقط اجزای همین فرم Validate شود و فرم دوم بدون تغییر باقی بماند.
ویوی زیر را در نظر بگیرید. در layout، دو پارشال، به
کمک اکشنمتد فراخوانی شدهاند:
ViewModelهای مرتبط با این دو بخش به شکل زیر هستند :
ContactVM .cs
public class ContactVM { [Display(Name = "نام")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string Name { get; set; } [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")] [DataType(DataType.EmailAddress)] [Display(Name = "آدرس ایمیل")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string EmailAddress { get; set; } [Display(Name = "متن پیام")] [Required(ErrorMessage = "حرفی برای گفتن ندارید؟")] public string Description { get; set; } [Required(ErrorMessage = "لطفا {0} را وارد کنید")] [Display(Name = "حاصل جمع")] public string Captcha { get; set; } }
SubscriberVM .cs
public class SubscriberVM { /*[RegularExpression("^[a-zA-Z0-9_\\.-]+@([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,6}$", ErrorMessage = "آدرس ایمیل صحیح نیست")]*/ [EmailAddress(ErrorMessage = "آدرس ایمیل صحیح نیست")] /*.Net4.5*/ [Display(Name = "ایمیل")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string Email { get; set; } [Display(Name = "وضعیت")] public bool IsActive { get; set; } }
در Layout، دو اکشن متد صدا زده شدهاند که وظیفه ارسال ویوهای هر کدام به Layout را به عهده دارند :
<div class="row footerclass"> <div class="col-md--6"> @Html.Action("Subscribers", "Home") </div> <div class="col-md-6"> @Html.Action("Contact", "Home") </div> </div>
اکشن متدهای این دو پارشال به شکل زیر هستند :
public ActionResult Contact() { return PartialView("_Contact", model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Contact(ContactVM model) { if (ModelState.IsValid) { //Do Something } return PartialView("_Contact", model); } public ActionResult Subscribers() { return PartialView("_Subscribers"); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model) { if (ModelState.IsValid) { //Do Something } } return PartialView("_Subscribers",model); }
و اما ویوهایی که قرار است نمایش داده شوند:
Contact.Cshtml _
@model IrsaShop.Models.ViewModel.ContactVM <span></span><span>تماس با ما</span> <hr /> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <div> @Html.TextBoxFor(m => m.Name, new { @class = "form-control", @id = "name", @name = "name", placeholder = "نام" }) @Html.ValidationMessageFor(m => m.Name) </div> <div> @Html.TextBoxFor(m => m.EmailAddress, new { @class = "form-control", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr" }) @Html.ValidationMessageFor(m => m.EmailAddress) </div> <div> @Html.TextAreaFor(model => model.Description, new { @class = "form-control", @id = "message", @name = "message", placeholder = "پیام", @style = "max-width: 100%;height: 90px;" }) </div> <div> <input type="button" value="" id="refresh" /> <img alt="Captcha" id="imgcpatcha" src="@Url.Action("CaptchaImage","Captcha")" /> </div> <div> @Html.TextBoxFor(model => model.Captcha, new { @class = "form-control", placeholder = "حاصل جمع؟" }) @Html.ValidationMessageFor(model => model.Captcha) </div> <div> <input type="submit" value="ارسال" name="submitValue" /> </div> }
_Subscriber.Csh tml
@model IrsaShop.Models.SubscriberVM <span></span><span>خبرنامه</span> <hr/> @using (Html.BeginForm()) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <div> <div> @Html.TextBoxFor(m => m.Email, new { @class = "form-control right-buffer top-buffer pull-right", @id = "email", @name = "email", placeholder = "ایمیل", @style = "direction: ltr;width: 50%", @required = "required" }) @* <button type="submit" name="submitValue">ثبت ایمیل</button>*@ <input type="submit" value="ثبت ایمیل" name="submitValue" /> </div> </div> @Html.ValidationMessageFor(m => m.Email,"",new { @class = "right-buffer pull-right"}) }
نکته اول : هیچ نوع ورودی برای Html.BeginForm در نظر گرفته نشده است.
اگر اکشن متدی را برای صدا زدن در این بخش در نظر بگیرید، هنگام Postback به مشکل
برخورد خواهید کرد؛ چون آدرس آن اکشن متد به شکل صریح در آدرس مرورگر فراخوانی میشود و
پارشال ما پس از Post-back به تنهایی
و بدون Layout نمایش داده خواهد شد. اسم بردن از اکشن متد وقتی کارساز است که آن اکشن متد
قرار باشد یک Redirect انجام دهد ولی هدف ما این
است که صفحه را از دست ندهیم و پیامهای خطای ModelState را در همان صفحه قبل و پس
از Post-back ببینیم و همچنین پس از انجام عملیات (مثلا
ارسال پیام) همین صفحه نمایش داده شود.
نکته دوم : نکته اول یک مشکل دارد! اگر به شکل صریح اکشن متد مربوط به Post-back مشخص نشود، بطور اتوماتیک تمامی اکشن متدهایی که ویژگی [HttpPost] دارند اجرا خواهند شد. این یعنی هر دو اکشن متد Contact و Subscriber اجرا میشوند و بنابر آنچه در اکشن متدها نوشتهایم هر دو ModelState بررسی میشود که این هدف ما نیست. مثلا فرم سمت چپ را تکمیل کرده ایم و دکمه "ثبت ایمیل" را فشار دادهایم و صفحه Postback میشود و با اینکه ایمیل در بانک ثبت شده اما فرم سمت راستی با خطا ظاهر میشود که چرا فیلدها خالی هستند!؟
برای حل این مشکل کافیست خاصیت name مربوط به دکمهها را به شکل یک ورودی برای اکشن متدها بفرستیم و بر اساس وضعیت آن تنها state مدل مورد نظر خودمان را بررسی کنیم. پس اصلاح زیر را برای اکشن متدهای دارای ویژگی [HttpPost] انجام میدهیم.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Contact(ContactVM model, , string submitValue) { if (submitValue == "ارسال") { if (ModelState.IsValid) { //Do Something } } else { ModelState.Clear(); } return PartialView("_Subscribers", model); } [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model, string submitValue) { if (submitValue == "ثبت ایمیل") { if (ModelState.IsValid) { //Do Something } } else { ModelState.Clear(); } return PartialView("_Subscribers"); }
نکته سوم : در این روش سعی کنید از ViewModel استفاده کنید و سعی کنید ویو مدلها پراپرتیهای با نام یکسان نداشته باشند. مثلا پراپرتی Email در ویو مدلها نامهای متفاوتی داشته باشند (مثل EmailAddress ، Email ، ContactMail و ...). با اینکار در زمان Postback احتمال اینکه فیلدهای مشترک اتوماتیک پر شده به ما نمایش داده باشند صفر خواهد شد.
نکته چهارم : حواستان باشد پس از انجام عملیات مرتبط با هر فرم در اکشن متد مربوط به آن (مثلا ارسال ایمیل، ثبت در بانک یا ...) در صورتی که عملیات با موفقیت انجام شد حتما ModelState را clear کنید. با اینکار پس از Post-back فیلدهای پارشالها خالی میشوند.
نکته پنجم : میتوانید به سادگی مدیریت خطا را به کمک جی کوئری انجام دهید؛ مثلا فرض کنید میخواهیم اگر ایمیل کاربر برای دریافت خبرنامه با موفقیت ثبت شد، پیامی مبنی بر موفقیت برای وی بفرستیم؛ اکشن متد HttPost مربوط به Subscriber را به شکل زیر تکمیل میکنیم :
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public ActionResult Subscribers(SubscriberVM model, string submitValue) { if (submitValue == "ثبت ایمیل") { if (ModelState.IsValid) { Subscriber mail = new Subscriber() { Email = model.EmailSubscriber, IsActive = true }; context.Subscribers.Add(mail); context.SaveChanges(); ViewBag.info = "ایمیل شما با موفقیت ثبت شد."; ViewBag.color = "alert-success"; ModelState.Clear(); } } else { ModelState.Clear(); } return PartialView("_Subscribers "); }
در انتهای پارشال _Subscriber هم چند خط کد زیر را مینویسیم :
@if (!String.IsNullOrEmpty(ViewBag.info)) { <div id="info" style="position: fixed; bottom: 0; right: 0; margin-right: 1%;"> <div class="alert @ViewBag.color alert-dismissable"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <strong> @ViewBag.info</strong> </div> </div> <script> $(function () { $("#info").fadeOut(15000); }); </script> }
نتیجه این خواهد بود که پس از PostBack در صورت موفقیت تصویر زیر را خواهیم دید و 15 ثانیه المان سبزرنگ بوت استرپِ زیر نمایش داده خواهد شد.
این روش نوعی مدیریت میان اکشن متدهای دارای ویژگی HttpPost است و همانطور که گفتیم به علت اینکه پس از Post-Back نیاز به ساختار به هم نخوردهی صفحهی قبلی داریم، نمیتوانستیم به شکل صریح، اکشن متد برای Html.BeginForm تعریف کنیم تا این دردسرها را نداشته باشیم.
حین نوشتن این مقاله به علت وجود if های تو در تو، امیدوار بودم که روشهای بهتری برای اینکار موجود باشند و هنوز هم امیدوارم نظرات شما چنین چیزی را نشان دهد.
در مورد static reflection مقدمهای پیشتر در این سایت قابل مطالعه است (^) و پیشنیاز بحث جاری است. در ادامه قصد داریم یک سری از کاربردهای متداول آنرا که این روزها در گوشه و کنار وب یافت میشود، به زبان ساده بررسی کنیم.
بهبود کدهای موجود
از static reflection در دو حالت کلی میتوان استفاده کرد. یا قرار است کتابخانهای را از صفر طراحی کنیم یا اینکه خیر؛ کتابخانهای موجود است و میخواهیم کیفیت آنرا بهبود ببخشیم. هدف اصلی هم «حذف رشتهها» و «استفاده از کد بجای رشتهها» است.
برای مثال قطعه کد زیر یک مثال متداول مرتبط با WPF و یا Silverlight است. در آن با پیاده سازی اینترفیس INotifyPropertyChanged و استفاده از متد raisePropertyChanged ، به رابط کاربری برنامه اعلام خواهیم کرد که لطفا خودت را بر اساس اطلاعات جدید تنظیم شده در قسمت set خاصیت Name ، به روز کن:
using System.ComponentModel;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
تعاریف قسمت PropertyChangedEventArgs این پیاده سازی، خارج از کنترل ما است و در دات نت فریم ورک تعریف شده است. حتما هم نیاز به رشته دارد؛ آن هم نام خاصیتی که تغییر کرده است. چقدر خوب میشد اگر میتوانستیم این رشته را حذف کنیم تا کامپایلر بتواند صحت بکارگیری اطلاعات وارد شده را دقیقا پیش از اجرای برنامه بررسی کند. الان فقط در زمان اجرا است که متوجه خواهیم شد، مثلا آیا به روز رسانی مورد نظر صورت گرفتهاست یا خیر؛ اگر نه، یعنی احتمالا یک اشتباه تایپی جایی وجود دارد.
برای بهبود این کد همانطور که در قسمت قبل نیز گفته شد، از ترکیب کلاسهای Expression و Func استفاده خواهیم کرد. در اینجا Func قرار نیست چیزی را اجرا کند، بلکه از آن به عنوان قطعه کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده میشود. این استخراج اطلاعات هم توسط کلاس Expression انجام میشود. بنابراین قسمت اول بهبود کد به صورت زیر شروع میشود:
void raisePropertyChanged(Expression<Func<object>> expression)
الان اگر متد raisePropertyChanged بکارگرفته شده در خاصیت Name را بخواهیم اصلاح کنیم، حداقل با دو واقعهی مطلوب زیر مواجه خواهیم شد:
Intellisense به صورت خودکار کار میکند:
حتی بدویترین ابزارهای Refactoring موجود (منظور همان ابزار توکار VS.NET است!) هم امکان Refactoring را در اینجا فراهم خواهند ساخت:
در پایان کد تکمیل شده فوق به شرح زیر خواهد بود که در آن از کلاس Expression جهت استخراج Member.Name استفاده شده است:
using System;
using System.ComponentModel;
using System.Linq.Expressions;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged(() => Name);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(Expression<Func<object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
}
}
کتاب درک رمزنگاری اطلاعات
Cryptography has crept into everything, from web browsers and email programs to cell phones, bank cards, cars and even into medical implants. Thus, an increasing number of people have to understand how crypto schemes work and how they can be used in practice. We wanted to create a book that teaches modern applied cryptography to readers with a technical background but without an education in pure mathematics.
fields: { "Price": { type: "number", validation: { required: true, min: 1 } } }
Kendo UI Validation و HTML 5
در HTML 5 امکان تعریف نوعهای خاص کنترلهای ورودی کاربر مانند email، url، number، range، date، search و color وجود دارد. برای مثال در اینجا اگر کاربر تاریخ غیرمعتبری را وارد کند، مرورگر پیام اعتبارسنجی متناظری را به او نمایش خواهد داد. همچنین در HTML 5 امکان افزودن ویژگی required نیز به کنترلهای ورودی پیش بینی شدهاست. اما باید درنظر داشت که مرورگرهای قدیمی از این امکانات پشتیبانی نمیکنند. در این حالت Kendo UI با تشویق استفاده از روش معرفی شده در HTML 5، با آن یکپارچه شده و همچنین این قابلیتهای اعتبارسنجی HTML 5 را در مرورگرهای قدیمی نیز میسر میکند. Kendo UI Validation جزو نسخهی سورس باز Kendo UI با مجوز Apache نیز میباشد.
نمونهای از امکانات اعتبارسنجی توکار HTML 5 را در اینجا مشاهده میکنید:
<input type="text" name="firstName" required /> <input type="text" name="twitter" pattern="https?://(?:www\.)?twitter\.com/.+i" /> <input type="number" name="age" min="1" max="42" /> <input type="number" name="age" min="1" max="100" step="2" /> <input type="url" name="url" /> <input type="email" name="email" />
یکپارچه سازی اعتبارسنجی Kendo UI با اعتبارسنجی HTML 5
در اینجا یک فرم تشکیل شده با ساختار HTML 5 را ملاحظه میکنید. هر دو فیلد ورودی، با ویژگی استاندارد required مزین شدهاند. همچنین توسط ویژگی type، ورودی دوم جهت دریافت آدرس ایمیل معرفی شدهاست.
چون فیلد دوم دارای دو اعتبارسنجی تعریف شده است، دارای دو ویژگی *-data برای تعریف پیامهای اعتبارسنجی متناظر نیز میباشد. الگوی تعریف آنها data-[rule]-msg است.
<div class="k-rtl"> <form id="testView"> <label for="firstName">نام</label> <input id="firstName" name="firstName" type="text" class="k-textbox" required validationmessage="لطفا نامی را وارد کنید"> <br> <label for="emailId">آدرس پست الکترونیک</label> <input id="emailId" name="emailId" type="email" dir="ltr" required class="k-textbox" data-required-msg="لطفا ایمیلی را وارد کنید." data-email-msg="ایمیل وارد شده معتبر نیست."> <br> <input type="submit" class="k-button" value="ارسال"> </form> </div> <script type="text/javascript"> $(function () { $("form#testView").kendoValidator(); }); </script>
تعیین محل نمایش پیامهای اعتبارسنجی
پیامهای اعتبارسنجی Kendo UI به صورت خودکار در کنار فیلد متناظر با آن نمایش داده میشوند. اما اگر نیاز به تعیین مکان دستی آنها وجود داشت (جهت خوانایی بهتر) باید به نحو ذیل عمل کرد:
<input type="text" id="name" name="name" required> <span class="k-invalid-msg" data-for="name"></span>
تعریف سراسری پیامهای اعتبارسنجی
در مثال فوق، به ازای تک تک فیلدهای ورودی، پیام اعتبارسنجی متناظر با required وارد شد. میتوان این پیامها را حذف کرد و در قسمت messages متد kendoValidator قرار داد:
<script type="text/javascript"> $(function () { $("form#testView").kendoValidator({ messages: { // {0} would be replaced with the input element's name required: '{0} را تکمیل کنید.', email: 'ایمیل وارد شده معتبر نیست.' } }); }); </script>
- در این پیامها {0} با مقدار ویژگی name فیلد ورودی متناظر جایگزین میشود.
- اگر هم در markup و هم در تعاریف kendoValidator، پیامهای اعتبارسنجی تعریف شوند، حق تقدم با تعاریف markup خواهد بود.
اعتبارسنجی سفارشی سمت کاربر
علاوه بر امکانات استاندارد HTML 5، امکان تعریف دستورهای اعتبارسنجی سفارشی نیز وجود دارد:
<script type="text/javascript"> $(function () { $("form#testView").kendoValidator({ rules: { customRule1: function (input) { if (!input.is("[id=firstName]")) return true; var re = /^[A-Za-z]+$/; return re.test(input.val()); } //, customRule1: …. }, messages: { // {0} would be replaced with the input element's name required: '{0} را تکمیل کنید.', email: 'ایمیل وارد شده معتبر نیست.', customRule1: 'اعداد مجاز نیستند.' } }); }); </script>
- همانطور که ملاحظه میکنید، برای تعریف منطق اعتبارسنجی سفارشی، باید از خاصیت rules ورودی متد kendoValidator شروع کرد. در اینجا نام یک متد callback دلخواهی را وارد کرده و سپس بر اساس منطق اعتبارسنجی مورد نظر، باید true/false را بازگشت داد. برای نمونه در این مثال اگر کاربر در فیلد نام، عدد وارد کند، ورودی او مورد قبول واقع نخواهد شد.
- باید دقت داشت که اگر بررسی input.is صورت نگیرد، منطق تعریف شده به تمام کنترلهای صفحه اعمال میشود.
- پیام متناظر با این دستور سفارشی جدید، در قسمت messages، دقیقا بر اساس نام callback method تعریف شده در قسمت rules باید تعریف شود.
فراخوانی دستی اعتبارسنجی یک فرم
در حالت پیش فرض، با کلیک بر روی دکمهی ارسال، اعتبارسنجی کلیه عناصر فرم به صورت خودکار انجام میشود. اگر بخواهیم در این بین یک پیام سفارشی را نیز نمایش دهیم میتوان به صورت زیر عمل کرد:
<script type="text/javascript"> $(function () { $("form#testView").submit(function (event) { event.preventDefault(); var validator = $("form#testView").data("kendoValidator"); if (validator.validate()) { alert("validated!"); } else { alert("There is invalid data in the form."); } }); $("form#testView").kendoValidator(); }); </script>
اعتبارسنجی سفارشی در DataSource
در تعریف فیلدهای مدل DataSource، امکان تعریف اعتبارسنجیهای پیش فرضی مانند rquired، min، max و امثال آن وجود دارد که نمونهای از آنرا در بحث فعال سازی CRUD در Kendo UI Grid مشاهده کردید:
fields: { "serviceName": { type: "string", defaultValue: "Inspection", editable: true, nullable: false, validation: { /*...*/ } }, // ... }
schema: { model: { id: "ProductID", fields: { ProductID: { editable: false, nullable: true }, ProductName: { validation: { required: true, custom1: function (input) { if (input.is("[name='ProductName']") && input.val() != "") { input.attr("data-custom1-msg", "نام محصول باید با حرف بزرگ انگلیسی شروع شود"); return /^[A-Z]/.test(input.val()); } return true; } // ,custom2: ... } }, UnitPrice: { type: "number", validation: { required: true, min: 1} }, Discontinued: { type: "boolean" }, UnitsInStock: { type: "number", validation: { min: 0, required: true} } } } }
متد ()input.val مقدار کنترل جاری را بر میگرداند. برای دسترسی به مقدار سایر کنترلها میتوان از روش ()fieldName").val#")$ استفاده کرد.