استفاده از چند فرم در کنار هم در ASP.NET MVC
نویسنده: مرتضی دلیل
تاریخ: ۱۳۹۳/۰۳/۲۸ ۱۷:۳۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
اجرای این نوع صفحات کار سختی نیست؛ با کمی جستجو در اینترنت مثلا در اینجا میتوانید چیزهای خوبی پیدا کنید. اما متاسفانه اکثر مثالها چیزی شبیه قرار دادن پارشال "ورود اعضا" در کنار پارشال "ثبت نام" هستند. حتما متوجه شدهاید که معمولا این دو صفحه پس از 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 های تو در تو، امیدوار بودم که روشهای بهتری برای اینکار موجود باشند و هنوز هم امیدوارم نظرات شما چنین چیزی را نشان دهد.
public ActionResult Contact()
{
return PartialView("_Contact", model);
} @model IrsaShop.Models.ViewModel.ContactVM
@model IrsaShop.Models.SubscriberVM
@model IrsaShop.Models.ViewModel.؟
حین فراخوانی متد Html.Action، یک متد در یک کنترلر فراخوانی خواهد شد (که شامل ارائه درخواست و طی سیکل کامل پردازشی آن کنترلر نیز خواهد بود). سپس آن متد با بازگشت دادن یک PartialView، اطلاعات پردازش شده یک partial view را به فراخوان بازگشت میدهد
return PartialView("_Contact", model); if (this.ModelState.IsValid)
{
return RedirectToAction(actionName: "show", controllerName:"Posts", routeValues: new { id });
} child action قراره فقط محتوای قسمت کوچکی از صفحه رو تامین کنه. در اینجا redirect کل صفحه معنی نداره چون هنوز کار رندر صفحه تموم نشده و وسط کار هست.
در مثال بالا نیازی به استفاده از Html.Action و child action نبوده. از Html.Action زمانی استفاده میشه که قراره چند partial view از پیش مقدار دهی شده در صفحه، از منابع داده مختلفی مقدار دهی بشن. مثلا قراره در کنار صفحه، از یک منبع داده، آمار سایت رو نمایش بدید (اطلاعات از پیش مقدار دهی شده) و از یک منبع دیگه، لیست مطالب و این دو مورد هم قراره در layout سایت قرار بگیرن. اینجا شما ویرایش اطلاعات رو که ندارید (برای مقدار دهی عناصر فرم از قبل). فقط قراره از کاربر اطلاعات بگیرید. بنابراین استفاده از رندر کردن پارشال مثل قبل کار میکنه. مهم هم نیست که هر پارشالی مدل مختلفی داره. برای ارسال اطلاعات فرم به سرور، فقط نام کنترلها مهم است و نه نوع مدل شما. پروتکل HTTP چیزی از نوع مدل نمیدونه. ASP.NET MVC هست که نامها رو میگیره و متصل میکنه به خواص یک شیء.
در کل مثال بالا برای حالت قرار دادن این فرمها در layout سایت برای تمام صفحات طراحی شده. اگر یک صفحه معمولی و view معمولی هست، در ASP.NET MVC مجاز هستید n تا فرم در صفحه قرار بدید. برای اینکار نیازی به Html.Action نیست؛ با رندر پارشال هم کار میکنه. برای تامین داده اونها هم از ViewModel استفاده کنید. این ViewModel یک خاصیتش، کل اطلاعات فرم یک هست و خاصیت دومش کل اطلاعات فرم 2 و الی آخر.
[Authorize(Roles = "Administrator")]
public ActionResult AddUserRole()
{
return View();
}
//////////////////////////////////////////////////////////
//اضافه کردن نقش////////////
[Authorize(Roles = "Administrator")]
[HttpGet]
[ChildActionOnly]
public ActionResult AddRole()
{
return PartialView("PartialRole");
}
[ChildActionOnly]
[HttpPost]
//[ValidateAntiForgeryToken]
public ActionResult AddRole(tblRole rl, string submitValue)
{
if (submitValue == "ذخیره نقش")
{
//تبدیل تاریخ میلادی به شمسی
rl.RoleDateCreate = Utility.ToPersianDate(DateTime.Now);
UserRepository user = new UserRepository();
//برای وارد کردن خودکار شناسه کاربر وارد شده
rl.UserIDCreate_FK = Convert.ToInt64(user.FindUserID(User.Identity.Name));
if (ModelState.IsValid)
{
RoleRepository acr = new RoleRepository();
var model = acr.Add(rl);
ViewBag.Message = "نقش با موفقیت ثبت شد";
ModelState.Clear();
}
}
else
{
ModelState.Clear();
}
return PartialView("PartialRole", rl);
}
//////////////////////////////////////////////////////////
//اضافه کردن کاربر//////////
[Authorize(Roles = "Administrator")]
[HttpGet]
[ChildActionOnly]
public ActionResult AddUser()
{
return PartialView("PartialUser");
}
[ChildActionOnly]
[HttpPost]
public ActionResult AddUser(tblUser rl, string submitValue)
{
if (submitValue == "ثبت نام")
{
if (ur.ExistUserName(rl.UserName))//برای اینکه نام کاربری تکراری نباشد
{
ViewBag.Warning = "این نام کاربری تکراری میباشد";
}
else
{
//برای وارد کردن خودکار شناسه کاربر وارد شده
rl.UserIDCreate = Convert.ToInt64(ur.FindUserID(User.Identity.Name));
//تبدیل میلادی به شمسی
rl.UserDateCreate = Utility.ToPersianDate(DateTime.Now);
if (ModelState.IsValid)
{
UserRepository acr = new UserRepository();
var model = acr.Add(rl);
ViewBag.Message = "کاربر با موفقیت ثبت شد";
ModelState.Clear();
}
}
}
else
{
ModelState.Clear();
}
return PartialView("PartialUser", rl);
}
//////////////////////////////////////////////////////////
// دادن نقش به کاربر///////
[ChildActionOnly]
[HttpGet]
[Authorize(Roles = "Administrator")]
public ActionResult RoleOfUser()
{
var blUser = new UserRepository();
var blRole = new RoleRepository();
var model = new RoleUserViewModel();
model.username = blUser.Select().ToList();
model.rolename = blRole.Select().ToList();
return PartialView("PartialRoleOfUser", model);
}
[ChildActionOnly]
[HttpPost]
public ActionResult RoleOfUser(tblRoleUser rl, string submitValue)
{
if (submitValue == "ذخیره نقش کاربر")
{
//برای وارد کردن خودکار شناسه کاربر وارد شده
rl.UserIDCreate_FK = Convert.ToInt64(ur.FindUserID(User.Identity.Name));
//تبدیل میلادی به شمسی
rl.RoleUserDateCreate = Utility.ToPersianDate(DateTime.Now);
if (ModelState.IsValid)
{
RoleUserRepository acr = new RoleUserRepository();
var model = acr.Add(rl);
ViewBag.Message = "نقش کاربر با موفقیت ثبت شد";
ModelState.Clear();
}
}
else
{
ModelState.Clear();
}
return PartialView("PartialRoleOfUser", rl);
} @model MeterControl.ViewModels.RoleUserViewModel
<script src="~/Scripts/jquery-1.8.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
@using (Ajax.BeginForm("AddUserRole", "Home", new AjaxOptions { HttpMethod = "Post", Url = "/Home/AddUserRole" })){
<fieldset>
<legend>tblRole</legend>
<div>
@Html.LabelFor(model => model.role.RoleName)
</div>
<div>
@Html.EditorFor(model => model.role.RoleName)
@Html.ValidationMessageFor(model => model.role.RoleName)
</div>
@ViewBag.Message
<p>
<input type="submit" value="ذخیره نقش" name="submitValue" />
</p>
</fieldset>
} @{
ViewBag.Title = "AddUserRole";
}
<h2>AddUserRole</h2>
<div>
<div>
@Html.Action("AddRole", "Home")
</div>
<div>
@Html.Action("AddUser", "Home")
@Html.Partial("PartialUser")
</div>
<div>
@Html.Action("RoleOfUser","Home")
</div>
</div> @Html.Action("AddRole", "Home") [HttpGet] را از روی AddRole اول حذف کنید.
ضمنا زمانیکه فقط قصد نمایش یک Partial view را دارید، نیازی نیست حتما از Html.Action استفاده کنید؛ چون سربار بالایی دارد. هر بار فراخوانی Html.Action سبب راه اندازی مجدد سیکل پردازشی کنترلر جاری میشه. اگر فقط قصد نمایش ساده اینها است (بدون پاس دادن اطلاعات اضافهتری به آنها)، از همان Html.Partial استفاده کنید. برای مثال در کدهای فوق سه مورد AddRole ، AddUser و RoleOfUser اضافی هستند. حذفشون کنید و اینها را با RenderPartial داخل View تعویض کنید. اون مورد RoleOfUser که پارامتر دریافت میکنه رو با یک ViewModel اطلاعاتش رو به View برای رندر ارسال کنید.
public class RoleUserViewModel
{
public tblRole role { get; set; }
public tblUser user { get; set; }
public tblRoleUser roleuser { get; set; }
//برای اینکه در صفحه برای نام کاربری و نام نقش دراپ دان لیست بگذاریم
public IEnumerable<tblUser> username { get; set; }
public IEnumerable<tblRole> rolename { get; set; }
}
مشکل syntax داره. یکی از دو حالت زیر را استفاده کنید:
@{ Html.RenderPartial("......."); } @{ Html.RenderPartial(".......", Model); }
«هیچ اکشنی رو فراخوانی نمیکنه»
فرقش با قبل فقط همین یک مورد هست. مابقیش فرقی نمیکنه. RenderAction کارش فقط رندر هست و نه ثبت چیزی در دیتابیس. من فقط عنوان کردم ()ActionResult AddUser اضافی هست و سادهتر هم میشه مدیریتش کرد. متد (ActionResult AddUser(tblUser rl, string submitValue بدیهی هست که باید مانند قبل برای مدیریت Postها باشد.