مطالب
ASP.NET MVC #16

مدیریت خطاها در یک برنامه ASP.NET MVC


استفاده از فیلتر HandleError

یکی از فیلترهای توکار ASP.NET MVC به نام HandleError،‌ می‌تواند کار هدایت کاربر را به یک صفحه‌ی خطای عمومی، در حین بروز استثنایی در برنامه،‌ انجام دهد. برای آزمایش آن یک برنامه خالی جدید ASP.NET MVC را آغاز کنید. سپس یک کنترلر جدید را با محتوای زیر به آن اضافه نمائید:

using System;
using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class HomeController : Controller
{
[HandleError]
public ActionResult Index()
{
throw new InvalidOperationException();
return View();
}
}
}

در اینجا جهت آزمایش برنامه، به عمد یک استثنای دستی را صادر می‌کنیم. برای آزمایش برنامه هم نیاز است آن‌را خارج از دیباگر VS.NET اجرا کرد (آدرس برنامه را مستقیما خارج از VS.NET در یک مرورگر وارد کنید). همچنین یک سطر زیر را نیز لازم است به فایل web.config برنامه اضافه نمائید:

<system.web>
<customErrors mode="On" />

اکنون اگر برنامه را خارج از مرورگر اجرا کنید، با توجه به استفاده از ویژگی HandleError و همچنین بروز یک استثنا در متد Index، خودبخود صفحه Views\Shared\Error.cshtml به کاربر نمایش داده خواهد شد. در غیراینصورت صفحه زرد رنگ پیش فرض خطای ASP.NET به کاربر نمایش داده می‌شود که محتوای آن‌ها بیشتر برای برنامه نویس‌ها مناسب است و نه کاربران نهایی سیستم.
اگر علاقمند باشید که این ویژگی به صورت خودکار به تمام متدهای کنترلرهای برنامه اعمال شود، کافی است یک سطر زیر را به متد Application_Start فایل Global.asax.cs اضافه نمائید:

GlobalFilters.Filters.Add(new HandleErrorAttribute());

البته نیازی به انجام اینکار نیست زیرا اگر به متد RegisterGlobalFilters فایل Global.asax.cs دقت کنیم، اینکار پیشتر توسط قالب پیش فرض VS.NET انجام شده است. فقط برای فعال سازی آن نیاز است تگ customErrors در فایل وب کانفیگ برنامه مقدار دهی و تنظیم شود.



استفاده از صفحه خطای سفارشی دیگری بجای فایل Error.cshtml

امکان تنظیم نمایش صفحه خطای سفارشی دیگری نیز وجود دارد. برای مثال استفاده از فایل Views\Shared\CustomErrorView.cshtml :

[HandleError(View = "CustomErrorView")]



استفاده از صفحات خطای متفاوت به ازای استثناهای مختلف

می‌توان فیلتر HandleError را تنها به یک نوع استثنای خاص محدود کرد. همچنین امکان استفاده از چندین ویژگی HandleError برای یک متد نیز وجود دارد:

[HandleError(ExceptionType = typeof(NullReferenceException), View = "ErrorHandling")]



دسترسی به اطلاعات استثناء در صفحه نمایش خطاها

زمانیکه برنامه به صفحه خطا هدایت می‌شود، نوع Model آن System.Web.Mvc.HandleErrorInfo می‌باشد:

@model System.Web.Mvc.HandleErrorInfo

@{
ViewBag.Title = "DbError";
}

<h2>An Error Has Occurred</h2>

@if (Model != null)
{
<p>@Model.Exception.GetType().Name<br />
thrown in @Model.ControllerName @Model.ActionName</p>
}

البته این نکته را صرفا به عنوان اطلاعات عمومی در نظر داشته باشید. زیرا اگر قرار باشد مجددا اصل استثناء را نمایش دهیم، همان صفحه زرد رنگ ASP.NET شاید بهتر باشد.



استفاده از تگ customErrors در فایل Web.config برنامه

ویژگی حالت تگ customErrors در فایل web.config برنامه، سه مقدار را می‌تواند بپذیرد:
الف) Off : صفحه زرد رنگ معرفی خطای ASP.NET را به همراه تمام اطلاعات مرتبط با استثنای رخ داده نمایش می‌دهد.
ب) RemoteOnly : همان حالت الف است با این تفاوت که صفحه خطا را فقط در کامپیوتری که وب سرور بر روی آن نصب است نمایش خواهد داد.
ج) On : یک صفحه خطای سفارشی شده را نمایش می‌دهد.

بنابراین هیچگاه از حالت Off استفاده نکنید. زیرا خطاهای نمایش داده شده، علاوه بر برنامه نویس، برای مهاجم به یک سایت نیز بسیار دلپذیر است!
حالت RemoteOnly در زمان توسعه برنامه توصیه می‌شود.
حالت On حین توزیع برنامه باید بکارگرفته شود.



مدیریت خطاهای رخ داده خارج از MVC Pipeline

HandleErrorAttribute تنها استثناهای رخ داده داخل ASP.NET MVC Pipeline را مدیریت می‌کند (یا خطاهایی از نوع 500). اگر این نوع استثناها خارج از آن رخ دهند مثلا فایلی یافت نشود (خطای 404) و امثال آن، باید به روش زیر عمل کرد:

<customErrors mode="On" defaultRedirect="error">
<error statusCode="404" redirect="error/notfound" />
<error statusCode="403" redirect="error/forbidden" />
</customErrors>

در اینجا اگر فایلی یافت نشد، کاربر به کنترلری به نام error و متدی به نام notfound هدایت خواهد شد. بنابراین نیاز به کنترلر زیر وجود دارد؛ به علاوه به ازای هر متد هم یک View متناظر باید اضافه شود (کلیک راست روی نام متد و انتخاب گزینه افزودن View جدید).

using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class ErrorController : Controller
{
public ActionResult Index()
{
return View();
}

public ActionResult NotFound()
{
return View();
}

public ActionResult Forbidden()
{
return View();
}
}
}

برای آزمایش این قسمت، برنامه را اجرا کرده و سپس مثلا آدرس غیرموجود http://localhost/xyz را وارد کنید.



استفاده از فیلتر HandleError اجباری نیست

در همین قسمت قبل پس از افزودن customErrors و defaultRedirect آن که به نام یک کنترلر اشاره می‌کند، کلیه فیلترهای HandleError اضافه شده به برنامه را حذف کنید. سپس برنامه را خارج از محیط VS.NET اجرا کنید. باز هم متد Index کنترلر Error اجرا خواهد شد. به عبارتی الزاما نیازی به استفاده از فیلتر HandleError نیست و به کمک مقدار دهی صحیح تگ customErrors، کار نمایش خودکار صفحه سفارشی خطاها به کاربر انجام خواهد شد.
البته بدیهی است که گزینه‌های نمایش یک View خاص به ازای استثنایی ویژه، یکی از مزیت‌های استفاده از فیلتر HandleError می‌باشد که امکان تنظیم آن در فایل web.config وجود ندارد.



ثبت اطلاعات استثناهای رخ داده به کمک ELMAH

نمایش صفحه‌ی خطای سفارشی به کاربر، یکی از موارد ضروری تمام برنامه‌های ASP.NET است، اما کافی نیست. ثبت اطلاعات جزئیات استثناهای رخ داده در طول زمان می‌توانند به بالا بردن کیفیت برنامه به شدت کمک کنند. برای این منظور می‌توان همانند سابق از متد Application_Error قابل تعریف در فایل Global.asax.cs کمک گرفت؛ اما با وجود افزونه‌ای به نام ELMAH اینکار اتلاف وقت است و اصلا توصیه نمی‌شود. همچنین به کمک ELMAH می‌توان مشکلات را تبدیل به ایمیل‌های خودکار کرد یا از آن‌ها فید RSS درست نمود.
برای دریافت ELMAH یا به سایت اصلی آن مراجعه نمائید و یا به کمک NuGet هم به سادگی قابل دریافت است. پس از دریافت، ارجاعی را به اسمبلی آن (Elmah.dll) اضافه نمائید. در ادامه فایل web.config برنامه را گشوده و چند سطر زیر را به آن در قسمت configuration اضافه کنید:

<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

سپس ذیل قسمت appSettings، تنظیمات پروایدر ذخیره سازی اطلاعات آن‌را وارد نمائید. مثلا در اینجا از فایل‌های XML برای ذخیره سازی اطلاعات استفاده خواهد شد (که امن‌ترین حالت ممکن است؛ از این لحاظ که اگر بانک اطلاعاتی را انتخاب کنید، ممکن است مشکل اصلی از همانجا ناشی شده باشد. بنابراین خطایی ثبت نخواهد شد. همچنین در این حالت نیازی به سایر DLLهای همراه ELMAH هم نیست). در اینجا مسیر ذخیره سازی اطلاعات در پوشه app_data/errorslog تنظیم شده است:

<elmah>
<security allowRemoteAccess="1"/>
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data/ErrorsLog"/>
</elmah>

در ادامه در قسمت system.web، دو تعریف زیر را اضافه نمائید. به این ترتیب امکان دسترسی به آدرس http://server/elmah.axd مهیا می‌گردد:

<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</httpModules>
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</httpHandlers>

البته برای IIS7 تنظیمات ذیل نیز باید اضافه شوند:

<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</modules>
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</handlers>
</system.webServer>

و به این ترتیب تنظیمات اولیه ELMAH به پایان می‌رسد (و با ASP.NET Web forms هیچ تفاوتی ندارد).
مرحله بعد، تنظیمات مسیریابی ASP.NET MVC است برای اینکه آدرس http://server/elmah.axd را وارد سیستم پردازشی خود نکند. البته اینکار پیشتر انجام شده است:

public static void RegisterRoutes(RouteCollection routes)
{
//routes.IgnoreRoute("elmah.axd");
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

بنابراین همین تنظیمات، به همراه قالب پیش فرض یک پروژه جدید ASP.NET MVC برای استفاده از ELMAH کفایت می‌کند. اکنون پروژه جاری را یکبار دیگر خارج از VS.NET اجرا کرده و سپس به مسیر http://localhost/elmah.axd جهت مشاهده خطاهای لاگ شده به همراه جزئیات کامل آن‌ها مراجعه کنید.

مشکل: استثناهای برنامه توسط ELMAH لاگ نمی‌شوند!

فیلتر HandleError با ELMAH سازگار نیست. زیرا با استفاده از آن، متدهای کنترلرها به صورت خودکار داخل یک try/catch اجرا شده و به این ترتیب استثناهای رخ داده، مدیریت گردیده و به ELMAH هدایت نمی‌شوند. بنابراین نیاز است به متد RegisterGlobalFilters فایل Global.asax.cs مراجعه کرده و سطر زیر را حذف کنید:

filters.Add(new HandleErrorAttribute());

و یا اگر قصد نداشتید اینکار را انجام دهید، می‌توان به نحو زیر نیز مشکل را حل کرد:

using System.Web.Mvc;
using Elmah;

namespace MvcApplication13.CustomFilters
{
public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.ExceptionHandled)
ErrorSignal.FromCurrentContext().Raise(context.Exception);
// all other exceptions will be caught by ELMAH anyway
}
}
}

در اینجا یک فیلتر سفارشی به برنامه اضافه شده است تا خطاهای مدیریت شده برنامه (خطاهای مدیریت شده توسط فیلتر HandleError توکار) را به موتور ELMAH هدایت کند. سایر خطاهای مدیریت نشده به صورت خودکار توسط ELMAH ثبت خواهند شد و نیازی به انجام کار اضافی در این مورد نیست.
سپس این فیلتر جدید را به صورت سراسری تعریف کنید:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ElmahHandledErrorLoggerFilter());
filters.Add(new HandleErrorAttribute());
}

ترتیب این‌ها هم مهم است. ابتدا باید ElmahHandledErrorLoggerFilter معرفی شود.


تذکر مهم!
حین استفاده از ELMAH یک نکته را فراموش نکنید:
اگر allowRemoteAccess آن‌را به عدد 1 تنظیم کرده‌اید، به هیچ عنوان از نام پیش فرض elmah.axd استفاده نکنید (هر نام اختیاری دیگری را که علاقمند بودید و به سادگی قابل حدس زدن نبود، در فایل web.config وارد کنید).


خلاصه بحث
1- در ASP.NET MVC نیازی نیست تا متدهای کنترلرها را با try/catch شلوغ کنید.
2- حتما قسمت customErrors فایل وب کانفیگ برنامه را دهی کنید (این مورد را به چک لیست اجباری تهیه یک برنامه ASP.NET MVC اضافه کنید).
3- استفاده از فیلتر HandleError اختیاری است. اگر از قابلیت فیلتر کردن استثناهای ویژه آن استفاده نمی‌کنید، مقدار دهی customErrors وب کانفیگ برنامه هم همان کار را انجام می‌دهد.
4- برای ثبت جزئیات دقیق استثناهای رخ داده در برنامه، از ELMAH استفاده کنید و بی‌جهت وقت خودتان را صرف بازنویسی این افزونه ارزشمند نکنید.

مطالب مشابه
معرفی ELMAH
ثبت استثناهای مدیریت شده توسط ELMAH

مطالب
ASP.NET MVC #10

آشنایی با روش‌های مختلف ارسال اطلاعات یک درخواست به کنترلر

تا اینجا با روش‌های مختلف ارسال اطلاعات از یک کنترلر به View متناظر آن آشنا شدیم. اما حالت عکس آن چطور؟ مثلا در ASP.NET Web forms، دوبار بر روی یک دکمه کلیک می‌کردیم و در روال رویدادگردان کلیک آن، همانند برنامه‌های ویندوزی، دسترسی به اطلاعات اشیاء قرار گرفته بر روی فرم را داشتیم. در ASP.NET MVC که کلا مفهوم Events را حذف کرده و وب را همانگونه که هست ارائه می‌دهد و به علاوه کنترلرهای آن، ارجاع مستقیمی را به هیچکدام از اشیاء بصری در خود ندارند (برای مثال کنترلر و متدی در آن نمی‌دانند که الان بر روی View آن، یک گرید قرار دارد یا یک دکمه یا اصلا هیچی)، چگونه می‌توان اطلاعاتی را از کاربر دریافت کرد؟
در اینجا حداقل سه روش برای دریافت اطلاعات از کاربر وجود دارد:
الف) استفاده از اشیاء Context مانند HttpContext، Request، RouteData و غیره
ب) به کمک پارامترهای اکشن متدها
ج) با استفاده از ویژگی جدیدی به نام Data Model Binding

یک مثال کاربردی
قصد داریم یک صفحه لاگین ساده را طراحی کنیم تا بتوانیم هر سه حالت ذکر شده فوق را در عمل بررسی نمائیم. بحث HTML Helpers استاندارد ASP.NET MVC را هم که در قسمت قبل شروع کردیم، لابلای توضیحات قسمت جاری و قسمت‌های بعدی با مثال‌های کاربردی دنبال خواهند شد.
بنابراین یک پروژه جدید خالی ASP.NET MVC را شروع کرده و مدلی را به نام Account با محتوای زیر به پوشه Models برنامه اضافه کنید:

namespace MvcApplication6.Models
{
public class Account
{
public string Name { get; set; }
public string Password { get; set; }
}
}

یک کنترلر جدید را هم به نام LoginController به پوشه کنترلرهای برنامه اضافه کنید. بر روی متد Index پیش فرض آن کلیک راست نمائید و یک View خالی را اضافه نمائید.
در ادامه به فایل Global.asax.cs مراجعه کرده و نام کنترلر پیش‌فرض را به Login تغییر دهید تا به محض شروع برنامه در VS.NET، صفحه لاگین ظاهر شود.
کدهای کامل کنترلر لاگین را در ادامه ملاحظه می‌کنید:

using System.Web.Mvc;
using MvcApplication6.Models;

namespace MvcApplication6.Controllers
{
public class LoginController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(); //Shows the login page
}

[HttpPost]
public ActionResult LoginResult()
{
string name = Request.Form["name"];
string password = Request.Form["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
[ActionName("LoginResultWithParams")]
public ActionResult LoginResult(string name, string password)
{
if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

[HttpPost]
public ActionResult Login(Account account)
{
if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}
}
}

همچنین Viewهای متناظر با این کنترلر هم به شرح زیر هستند:
فایل index.cshtml به نحو زیر تعریف خواهد شد:

@model MvcApplication6.Models.Account
@{
ViewBag.Title = "Index";
}
<h2>
Login</h2>
@using (Html.BeginForm(actionName: "LoginResult", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "LoginResultWithParams", controllerName: "Login"))
{
<fieldset>
<legend>Test LoginResult(string name, string password)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}
@using (Html.BeginForm(actionName: "Login", controllerName: "Login"))
{
<fieldset>
<legend>Test Login(Account acc)</legend>
<p>
Name: @Html.TextBoxFor(m => m.Name)</p>
<p>
Password: @Html.PasswordFor(m => m.Password)</p>
<input type="submit" value="Login" />
</fieldset>
}

و فایل result.cshtml هم محتوای زیر را دارد:

@{
ViewBag.Title = "Result";
}
<fieldset>
<legend>Login Result</legend>
<p>
@ViewBag.Message</p>
</fieldset>

توضیحاتی در مورد View لاگین برنامه:
در View صفحه لاگین سه فرم را مشاهده می‌کنید. در برنامه‌های ASP.NET Web forms در هر صفحه، تنها یک فرم را می‌توان تعریف کرد؛ اما در ASP.NET MVC این محدودیت برداشته شده است.
تعریف یک فرم هم با متد کمکی Html.BeginForm انجام می‌شود. در اینجا برای مثال می‌شود یک فرم را به کنترلری خاص و متدی مشخص در آن نگاشت نمائیم.
از عبارت using هم برای درج خودکار تگ بسته شدن فرم، در حین dispose شیء MvcForm کمک گرفته شده است.
برای نمونه خروجی HTML اولین فرم تعریف شده به صورت زیر است:

<form action="/Login/LoginResult" method="post">   
<fieldset>
<legend>Test LoginResult()</legend>
<p>
Name: <input id="Name" name="Name" type="text" value="" /></p>
<p>
Password: <input id="Password" name="Password" type="password" /></p>
<input type="submit" value="Login" />
</fieldset>
</form>

توسط متدهای کمکی Html.TextBoxFor و Html.PasswordFor یک TextBox و یک PasswordBox به صفحه اضافه می‌شوند، اما این For آن‌ها و همچنین lambda expression ایی که بکارگرفته شده برای چیست؟
متدهای کمکی Html.TextBox و Html.Password از نگارش‌های اولیه ASP.NET MVC وجود داشتند. این متدها نام خاصیت‌ها و پارامترهایی را که قرار است به آن‌ها بایند شوند، به صورت رشته می‌پذیرند. اما با توجه به اینکه در اینجا می‌توان یک strongly typed view را تعریف کرد،‌ تیم ASP.NET MVC بهتر دیده است که این رشته‌ها را حذف کرده و از قابلیتی به نام Static reflection استفاده کند (^ و ^).

با این توضیحات، اطلاعات سه فرم تعریف شده در View لاگین برنامه، به سه متد متفاوت قرار گرفته در کنترلری به نام Login ارسال خواهند شد. همچنین با توجه به مشخص بودن نوع model که در ابتدای فایل تعریف شده، خاصیت‌هایی را که قرار است اطلاعات ارسالی به آن‌ها بایند شوند نیز به نحو strongly typed تعریف شده‌اند و تحت نظر کامپایلر خواهند بود.


توضیحاتی در مورد نحوه عملکرد کنترلر لاگین برنامه:

در این کنترلر صرفنظر از محتوای متدهای آن‌ها، دو نکته جدید را می‌توان مشاهده کرد. استفاده از ویژگی‌های HttpPost، HttpGet و ActionName. در اینجا به کمک ویژگی‌های HttpGet و HttpPost در مورد نحوه دسترسی به این متدها، محدودیت قائل شده‌ایم. به این معنا که تنها در حالت Post است که متد LoginResult در دسترس خواهد بود و اگر شخصی نام این متدها را مستقیما در مرورگر وارد کند (یا همان HttpGet پیش فرض که نیازی هم به ذکر صریح آن نیست)، با پیغام «یافت نشد» مواجه می‌گردد.
البته در نگارش‌های اولیه ASP.NET MVC از ویژگی دیگری به نام AcceptVerbs برای مشخص سازی نوع محدودیت فراخوانی یک اکشن متد استفاده می‌شد که هنوز هم معتبر است. برای مثال:

[AcceptVerbs(HttpVerbs.Get)]

یک نکته امنیتی:
همیشه متدهای Delete خود را به HttpPost محدود کنید. به این علت که ممکن است در طی مثلا یک ایمیل، آدرسی به شکل http://localhost/blog/delete/10 برای شما ارسال شود و همچنین سشن کار با قسمت مدیریتی بلاگ شما نیز در همان حال فعال باشد. URL ایی به این شکل، در حالت پیش فرض، محدودیت اجرایی HttpGet را دارد. بنابراین احتمال اجرا شدن آن بالا است. اما زمانیکه متد delete را به HttpPost محدود کردید، دیگر این نوع حملات جواب نخواهند داد و حتما نیاز خواهد بود تا اطلاعاتی به سرور Post شود و نه یک Get ساده (مثلا کلیک بر روی یک لینک معمولی)، کار حذف را انجام دهد.


توسط ActionName می‌توان نام دیگری را صرفنظر از نام متد تعریف شده در کنترلر، به آن متد انتساب داد که توسط فریم ورک در حین پردازش نهایی مورد استفاده قرار خواهد گرفت. برای مثال در اینجا به متد LoginResult دوم، نام LoginResultWithParams را انتساب داده‌ایم که در فرم دوم تعریف شده در View لاگین برنامه مورد استفاده قرار گرفته است.
وجود این ActionName هم در مثال فوق ضروری است. از آنجائیکه دو متد هم نام را معرفی کرده‌ایم و فریم ورک نمی‌داند که کدامیک را باید پردازش کند. در این حالت (بدون وجود ActionName معرفی شده)، برنامه با خطای زیر مواجه می‌گردد:

The current request for action 'LoginResult' on controller type 'LoginController' is ambiguous between the following action methods:
System.Web.Mvc.ActionResult LoginResult() on type MvcApplication6.Controllers.LoginController
System.Web.Mvc.ActionResult LoginResult(System.String, System.String) on type MvcApplication6.Controllers.LoginController

برای اینکه بتوانید نحوه نگاشت فرم‌ها به متدها را بهتر درک کنید، بر روی چهار return View موجود در کنترلر لاگین برنامه، چهار breakpoint را تعریف کنید. سپس برنامه را در حالت دیباگ اجرا نمائید و تک تک فرم‌ها را یکبار با کلیک بر روی دکمه لاگین، به سرور ارسال نمائید.


بررسی سه روش دریافت اطلاعات از کاربر در ASP.NET MVC

الف) استفاده از اشیاء Context

در ویژوال استودیو، در کنترلر لاگین برنامه، بر روی کلمه Controller کلیک راست کرده و گزینه Go to definition را انتخاب کنید. در اینجا بهتر می‌توان به خواصی که در یک کنترلر به آن‌ها دسترسی داریم، نگاهی انداخت:

public HttpContextBase HttpContext { get; }
public HttpRequestBase Request { get; }
public HttpResponseBase Response { get; }
public RouteData RouteData { get; }

در بین این خواص و اشیاء مهیا، Request و RouteData بیشتر مد نظر ما هستند. در مورد RouteData در قسمت ششم این سری، توضیحاتی ارائه شد. اگر مجددا Go to definition مربوط به HttpRequestBase خاصیت Request را بررسی کنیم، موارد ذیل جالب توجه خواهند بود:

public virtual NameValueCollection QueryString { get; } // GET variables
public NameValueCollection Form { get; } // POST variables
public HttpCookieCollection Cookies { get; }
public NameValueCollection Headers { get; }
public string HttpMethod { get; }

توسط خاصیت Form شیء Request می‌توان به مقادیر ارسالی به سرور در یک کنترلر دسترسی یافت که نمونه‌ای از آن‌را در اولین متد LoginResult می‌توانید مشاهده کنید. این روش در ASP.NET Web forms هم کار می‌کند. جهت اطلاع این روش با ASP کلاسیک دهه نود هم سازگار است!
البته این روش آنچنان مرسوم نیست؛ چون NameValueCollection مورد استفاده، ایندکسی عددی یا رشته‌ای را می‌پذیرد که هر دو با پیشرفت‌هایی که در زبان‌های دات نتی صورت گرفته‌اند، دیگر آنچنان مطلوب و روش مرجح به حساب نمی‌آیند. اما ... هنوز هم قابل استفاده است.
به علاوه اگر دقت کرده باشید در اینجا HttpContextBase داریم بجای HttpContext. تمام این کلاس‌های پایه هم به جهت سهولت انجام آزمون‌های واحد در ASP.NET MVC ایجاد شده‌اند. کار کردن مستقیم با HttpContext مشکل بوده و نیاز به شبیه سازی فرآیندهای رخ داده در یک وب سرور را دارد. اما این کلاس‌های پایه جدید، مشکلات یاد شده را به همراه ندارند.


ب) استفاده از پارامترهای اکشن متدها

نکته‌ای در مورد نامگذاری پارامترهای یک اکشن متد به صورت توکار اعمال می‌شود که باید به آن دقت داشت:
اگر نام یک پارامتر، با نام کلید یکی از رکوردهای موجود در مجموعه‌های زیر یکی باشد، آنگاه به صورت خودکار اطلاعات دریافتی به این پارامتر نگاشت خواهد شد (پارامتر هم نام، به صورت خودکار مقدار دهی می‌شود). این مجموعه‌ها شامل موارد زیرهستند:

Request.Form
Request.QueryString
Request.Files
RouteData.Values

برای نمونه در متدی که با نام LoginResultWithParams مشخص شده، چون نام‌های دو پارامتر آن، با نام‌های بکارگرفته شده در Html.TextBoxFor و Html.PasswordFor یکی هستند، با مقادیر ارسالی آن‌ها مقدار دهی شده و سپس در متد قابل استفاده خواهند بود. در پشت صحنه هم از همان رکوردهای موجود در Request.Form (یا سایر موارد ذکر شده)، استفاده می‌شود. در اینجا هر رکورد مثلا مجموعه Request.Form، کلیدی مساوی نام ارسالی به سرور را داشته و مقدار آن هم، مقداری است که کاربر وارد کرده است.
اگر همانندی یافت نشد، آن پارامتر با نال مقدار دهی می‌گردد. بنابراین اگر برای مثال یک پارامتر از نوع int را معرفی کرده باشید و چون نوع int، نال نمی‌پذیرد، یک استثناء بروز خواهد کرد. برای حل این مشکل هم می‌توان از Nullable types استفاده نمود (مثلا بجای int id نوشت int? id تا مشکلی جهت انتساب مقدار نال وجود نداشته باشد).
همچنین باید دقت داشت که این بررسی تطابق‌های بین نام عناصر HTML و نام پارامترهای متدها، case insensitive است و به کوچکی و بزرگی حروف حساس نیست. برای مثال، پارامتر معرفی شده در متد LoginResult مساوی string name است، اما نام خاصیت تعریف شده در کلاس Account مساوی Name بود.


ج) استفاده از ویژگی جدیدی به نام Data Model Binding

در ASP.NET MVC چون می‌توان با یک Strongly typed view کار کرد، خود فریم ورک این قابلیت را دارد که اطلاعات ارسالی یکی فرم را به صورت خودکار به یک وهله از یک شیء نگاشت کند. در اینجا model binder وارد عمل می‌شود، مقادیر ارسالی را استخراج کرده (اطلاعات دریافتی از Form یا کوئری استرینگ‌ها یا اطلاعات مسیریابی و غیره) و به خاصیت‌های یک شیء نگاشت می‌کند. بدیهی است در اینجا این خواص باید عمومی باشند و هم نام عناصر HTML ارسالی به سرور. همچنین model binder پیش فرض ASP.NET MVC را نیز می‌توان کاملا تعویض کرد و محدود به استفاده از model binder توکار آن نیستیم.
وجود این Model binder، کار با ORMها را بسیار لذت بخش می‌کند؛ از آنجائیکه خود فریم ورک ASP.NET MVC می‌تواند عناصر شیءایی را که قرار است به بانک اطلاعاتی اضافه شود، یا در آن به روز شود، به صورت خودکار ایجاد کرده یا به روز رسانی نماید.
نحوه کار با model binder را در متد Login کنترلر فوق می‌توانید مشاهده کنید. بر روی return View آن یک breakpoint قرار دهید. فرم سوم را به سرور ارسال کنید و سپس در VS.NET خواص شیء ساخته شده را در حین دیباگ برنامه، بررسی نمائید.
بنابراین تفاوتی نمی‌کند که از چندین پارامتر استفاده کنید یا اینکه کلا یک شیء را به عنوان پارامتر معرفی نمائید. فریم ورک سعی می‌کند اندکی هوش به خرج داده و مقادیر ارسالی به سرور را به پارامترهای تعریفی، حتی به خواص اشیاء این پارامترهای تعریف شده، نگاشت کند.

در ASP.NET MVC سه نوع Model binder وجود دارند:
1) Model binder پیش فرض که توضیحات آن به همراه مثالی ارائه شد.
2) Form collection model binder که در ادامه توضیحات آن‌را مشاهده خواهید نمود.
3) HTTP posted file base model binder که توضیحات آن به قسمت بعدی موکول می‌شود.

یک نکته:
اولین متد LoginResult کنترلر را به نحو زیر نیز می‌توان بازنویسی کرد:
[HttpPost]
[ActionName("LoginResultWithFormCollection")]
public ActionResult LoginResult(FormCollection collection)
{
string name = collection["name"];
string password = collection["password"];

if (name == "Vahid" && password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

در اینجا FormCollection به صورت خودکار بر اساس مقادیر ارسالی به سرور توسط فریم ورک تشکیل می‌شود (FormCollection هم یک نوع model binder ساده است) و اساسا یک NameValueCollection می‌باشد.
بدیهی است در این حالت باید نگاشت مقادیر دریافتی، به متغیرهای متناظر با آن‌ها، دستی انجام شود (مانند مثال فوق) یا اینکه می‌توان از متد UpdateModel کلاس Controller هم استفاده کرد:

[HttpPost]
public ActionResult LoginResultUpdateFormCollection(FormCollection collection)
{
var account = new Account();
this.UpdateModel(account, collection.ToValueProvider());

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

متد توکار UpdateModel، به صورت خودکار اطلاعات FormCollection دریافتی را به شیء مورد نظر، نگاشت می‌کند.
همچنین باید عنوان کرد که متد UpdateModel، در پشت صحنه از اطلاعات Model binder پیش فرض و هر نوع Model binder سفارشی که ایجاد کنیم استفاده می‌کند. به این ترتیب زمانیکه از این متد استفاده می‌کنیم، اصلا نیازی به استفاده از FormCollection نیست و متد بدون آرگومان زیر هم به خوبی کار خواهد کرد:

[HttpPost]
public ActionResult LoginResultUpdateModel()
{
var account = new Account();
this.UpdateModel(account);

if (account.Name == "Vahid" && account.Password == "123")
ViewBag.Message = "Succeeded";
else
ViewBag.Message = "Failed";

return View("Result");
}

استفاده از model binderها همینجا به پایان نمی‌رسد. نکات تکمیلی آن‌ها در قسمت بعدی بررسی خواهند شد.

مطالب
ایجاد Drop Down List های آبشاری در Angular
تاکنون دو مطلب مشابه «ساخت DropDownList‌های مرتبط به کمک jQuery Ajax در MVC» و «ایجاد Drop Down List‌های آبشاری توسط Kendo UI» را در مورد ساخت Cascading Drop-down Lists در این سایت مطالعه کرده‌اید. در اینجا قصد داریم چنین قابلیتی را توسط Angular پیاده سازی کنیم (بدون استفاده از هیچ کتابخانه‌ی ثالث دیگری).



مدل‌های سمت سرور برنامه

در این مطلب قصد داریم لیست گروه‌ها را به همراه محصولات مرتبط با آن‌ها، توسط دو drop down list نمایش دهیم:
public class Category
{
    public int CategoryId { set; get; }
    public string CategoryName { set; get; }

    [JsonIgnore]
    public IList<Product> Products { set; get; }
}


public class Product
{
    public int ProductId { set; get; }
    public string ProductName { set; get; }
}
از ویژگی JsonIgnore جهت عدم درج لیست محصولات، در خروجی JSON نهایی تولیدی گروه‌ها، استفاده شده‌است (و کتابخانه‌ی JSON.NET، کتابخانه‌ی پیش فرض کار با JSON در ASP.NET Core است).


منبع داده JSON سمت سرور

پس از مشخص شدن مدل‌های برنامه، اکنون توسط دو اکشن متد، لیست گروه‌ها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت می‌دهیم:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        [HttpGet("[action]")]
        public async Task<IActionResult> GetCategories()
        {
            await Task.Delay(500);

            return Json(CategoriesDataSource.Items);
        }

        [HttpGet("[action]/{categoryId:int}")]
        public async Task<IActionResult> GetProducts(int categoryId)
        {
            await Task.Delay(500);

            var products = CategoriesDataSource.Items
                            .Where(category => category.CategoryId == categoryId)
                            .SelectMany(category => category.Products)
                            .ToList();
            return Json(products);
        }
    }
}
- بار اولی که صفحه بارگذاری می‌شود، توسط یک درخواست Ajax ایی، لیست گروه‌ها دریافت خواهد شد. سپس با انتخاب یک گروه، اکشن متد GetProducts جهت بازگرداندن لیست محصولات آن گروه، فراخوانی می‌گردد. کدهای کامل CategoriesDataSource در فایل پیوستی انتهای بحث قرار داده شده‌است و یک منبع ساده درون حافظه‌ای است.
- در اینجا از یک Delay نیز استفاده شده‌است تا بتوان آیکن‌های چرخند‌ه‌ی Loading سمت کاربر را در حین کار با عملیاتی زمانبر، بهتر مشاهده کرد.


 کدهای سمت کاربر برنامه

کدهای سمت کاربر این مثال در ادامه‌ی همان مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» هستند که بر روی آن این دستورات فراخوانی شده‌است:
 >ng g m Product -m app.module --routing
ماژول جدیدی به نام محصولات اضافه و به app.module معرفی شده‌است. البته پس از اصلاح، ProductModule بجای ProductRoutingModule در این فایل تنظیم خواهد شد.

 >ng g c product/product-group
سپس یک کامپوننت جدید به نام ProductGroupComponent درون ماژول Product ایجاد شده‌است.

>ng g cl product/product
>ng g cl product/Category
>ng g cl product/product-group-form
در ادامه سه کلاس Product، Category و ProductGroupForm به این ماژول اضافه شده‌اند که دو مورد اول، معادل کلاس‌های مدل سمت سرور و مورد سوم، معادل فرم جدید ProductGroupComponent است:
export class ProductGroupForm {
  constructor(
    public categoryId?: number,
    public productId?: number
  ) { }
}

export class Product {
  constructor(
    public productId: number,
    public productName: string
  ) { }
}

export class Category {
  constructor(
    public categoryId: number,
    public categoryName: string
  ) { }
}

سپس سرویسی را جهت دریافت اطلاعات دراپ داون‌ها از سرور تهیه کرده‌ایم:
 >ng g s product/product-items -m product.module
با این محتوا:
import { Injectable } from "@angular/core";
import { Http, Response, Headers, RequestOptions } from "@angular/http";

import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Category } from "./category";
import { Product } from "./product";

@Injectable()
export class ProductItemsService {

  private baseUrl = "api/product";

  constructor(private http: Http) { }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  getCategories(): Observable<Category[]> {
    return this.http
      .get(`${this.baseUrl}/GetCategories`)
      .map(response => response.json() || {})
      .catch(this.handleError);
  }

  getProducts(categoryId: number): Observable<Product[]> {
    return this.http
      .get(`${this.baseUrl}/GetProducts/${categoryId}`)
      .map(response => response.json() || {})
      .catch(this.handleError);
  }
}
از متد getCategories برای پر کردن اولین drop down استفاده خواهد شد و از متد دوم برای دریافت لیست محصولات متناظر با یک گروه انتخاب شده کمک می‌گیریم.

پس از این مقدمات اکنون می‌توان کدهای ProductGroupComponent را تکمیل کرد.
ابتدا در متد ngOnInit آن کار دریافت لیست آغازین گروه‌های محصولات را انجام می‌دهیم:
export class ProductGroupComponent implements OnInit {

  categories: Category[] = [];
 model = new ProductGroupForm();

  constructor(private productItemsService: ProductItemsService) { }

  ngOnInit() {
    this.productItemsService.getCategories().subscribe(
      data => {
        this.categories = data;
      },
      err => console.log("get error: ", err)
    );
  }
برای این منظور ابتدا ProductItemsService به سازنده‌ی کلاس تزریق شده‌است تا بتوان به متدهای دریافت اطلاعات از سرور دسترسی یافت. سپس در متد ngOnInit، اطلاعات دریافتی به خاصیت عمومی categories انتساب داده شده‌است.
اکنون چون این خاصیت در دسترس است، می‌توان به قالب این کامپوننت مراجعه کرده و قسمت ابتدایی فرم را تکمیل کرد:
<div class="container">
  <h3>Cascading Drop-down Lists</h3>
  <form #form="ngForm" (submit)="submitForm(form)" novalidate>
    <div class="form-group">
      <label class="control-label">Category</label>
      <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="categories.length == 0"></span>
      <select class="form-control" name="categoryCtrl" #categoryCtrl (change)="fetchProducts(categoryCtrl.value)"
        [(ngModel)]="model.categoryId">
        <option value="undefined">Select a Category...</option>
        <option *ngFor="let category of categories" value="{{category.categoryId}}">
          {{ category.categoryName }}
        </option>
      </select>
    </div>
- در اینجا اولین ngIf بکار گرفته شده، طول آرایه‌ی categories (همان خاصیت عمومی معرفی شده‌ی در کامپوننت) را بررسی می‌کند. اگر این آرایه خالی باشد، یک آیکن چرخنده را نمایش می‌دهد.
- سپس ngModel به خاصیت categoryId وهله‌ای از کلاس ProductGroupForm که مدل معادل فرم است، متصل شده‌است.
- همچنین با اتصال به رخداد change، مقدار Id عضو انتخابی به متد fetchProducts ارسال می‌شود. دسترسی به این Id از طریق یک template reference variable به نام categoryCtrl# انجام شده‌است.
- در آخر، ngFor تعریف شده به ازای هر عضو آرایه‌ی categories، یکبار تگ option را تکرار می‌کند و در هربار تکرار، مقدار ویژگی value را به categoryId تنظیم می‌کند و برچسب نمایشی آن‌را از categoryName دریافت خواهد کرد.

بنابراین مرحله‌ی بعدی تکمیل این drop down آبشاری، واکنش نشان دادن به رخ‌داد change و تکمیل متد fetchProducts است:
  products: Product[] = [];
  isLoadingProducts = false;

  fetchProducts(categoryId?: number) {
    console.log(categoryId);

    this.products = [];

    if (categoryId === undefined || categoryId.toString() === "undefined") {
      return;
    }

    this.isLoadingProducts = true;
    this.productItemsService.getProducts(categoryId).subscribe(
      data => {
        this.products = data;
        this.isLoadingProducts = false;
      },
      err => {
        console.log("get error: ", err);
        this.isLoadingProducts = false;
      }
    );
  }
- در ابتدای متد fetchProducts، آرایه‌ی خاصیت عمومی products که به drop down دوم متصل خواهد شد، خالی می‌شود تا تداخلی با اطلاعات قبلی آن حاصل نشود.
- سپس بررسی می‌کنیم که آیا categoryId دریافتی undefined است یا خیر؟ این مساله دو علت دارد:
الف) اولین عضو drop down انتخاب محصولات را با مقدار undefined مشخص کرده‌ایم:
 <option value="undefined">Select a Category...</option>
ب) علت اینجا است که چون ngModel به model.categoryId متصل شده‌است و در این مدل، پارامتر و همچنین خاصیت عمومی categoryId از نوع optional است و با ؟ مشخص شده‌است:
 public categoryId?: number
به همین جهت زمانیکه مدل را به این صورت تعریف می‌کنیم:
 model = new ProductGroupForm();
مقدار categoryId همان undefined جاوا اسکریپت خواهد بود.

- پس از آن همانند قسمت قبل، این categoryId را به سرور ارسال کرده و سپس اطلاعات متناظری را دریافت و به خاصیت عمومی products  نسبت داده‌ایم. همچنین از یک خاصیت عمومی دیگر به نام isLoadingProducts نیز استفاده شده‌است تا مشخص شود چه زمانی کار دریافت اطلاعات از سرور خاتمه پیدا می‌کند. از آن برای نمایش یک آیکن چرخنده‌ی دیگر استفاده می‌کنیم:
    <div class="form-group">
      <label class="control-label">Product</label>
      <span class="glyphicon glyphicon-refresh glyphicon-spin spinner" *ngIf="isLoadingProducts"></span>
      <select class="form-control" name="productCtrl" [(ngModel)]="model.productId">
        <option value="undefined">Select a Product...</option>
        <option *ngFor="let product of products" value="{{product.productId}}">
          {{ product.productName }}
        </option>
      </select>
    </div>
به این ترتیب drop down دوم بر اساس مقدار خاصیت عمومی products تشکیل می‌شود. اگر مقدار isLoadingProducts مساوی true باشد، یک spinner که کدهای css آن‌را در فایل src\styles.css به نحو ذیل تعریف کرده‌ایم، نمایان می‌شود و برعکس. همچنین ngFor به ازای هر عضو آرایه‌ی products یکبار تگ option را تکرار خواهد کرد.
/* Spinner */
.spinner {
  font-size:15px;
  z-index:10
}

.glyphicon-spin {
    -webkit-animation: spin 1000ms infinite linear;
    animation: spin 1000ms infinite linear;
}
@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}
@keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
        transform: rotate(0deg);
    }
    100% {
        -webkit-transform: rotate(359deg);
        transform: rotate(359deg);
    }
}

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-06.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
نوشتن TagHelperهای سفارشی برای ASP.NET Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» با مفهوم جدید Tag Helpers و همچنین نحوه‌ی استفاده‌ی از نمونه‌های پیش فرض و توکار آن در ASP.NET Core آشنا شدیم. در ادامه قصد داریم با نحوه‌ی پیاده سازی نمونه‌های سفارشی آن‌ها نیز آشنا شویم.


نوشتن یک Tag Helper سفارشی، برای رندر کردن لیست‌های بوت استرپی

فرض کنید می‌خواهیم یک tag helper جدید را جهت رندر کردن لیست بوت استرپی ذیل تهیه کنیم:
<ul class="list-group"> 
  <li class="list-group-item">Item 1</li> 
  <li class="list-group-item">Item 2</li> 
  <li class="list-group-item">Item 3</li> 
</ul>
برای اینکار یک کتابخانه‌ی جدید را به پروژه‌ی جاری اضافه کرده و سپس وابستگی‌های ذیل را نیز به آن اضافه می‌کنیم. این‌ها حداقل‌هایی هستند که جهت دسترسی به امکانات MVC و Tag Helpers، در یک پروژه‌ی مجزای Class library نیاز داریم:
{
  "version": "1.0.0-*",
 
    "dependencies": {
        "NETStandard.Library": "1.6.0",
        "Microsoft.AspNetCore.Http.Extensions": "1.0.0",
        "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.1",
        "Microsoft.AspNetCore.Mvc.Core": "1.0.1",
        "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.0.1",
        "Microsoft.AspNetCore.Razor.Runtime": "1.0.0"
    },
 
  "frameworks": {
    "netstandard1.6": {
      "imports": "dnxcore50"
    }
  }
}


بررسی آناتومی یک کلاس TagHelper

یک کلاس Tag Helper سفارشی، در حالت کلی می‌تواند شکل زیر را داشته باشد:
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
        }
    }
}
در اینجا نام کلاس، به TagHelper ختم می‌شود و همچنین این کلاس از کلاس پایه‌ی TagHelper ارث بری می‌کند. ذکر HtmlTargetElement الزامی بوده و در صورت عدم تعریف آن، TagHelper تعریف شده توسط ASP.NET Core شناسایی و بارگذاری نخواهد شد.
توسط HtmlTargetElement نام نهایی تگ مرتبط با TagHelper سفارشی را تعریف و سفارشی سازی کرده‌ایم. در این حالت این TagHelper جدید در Viewهای برنامه، توسط تگ ذیل شنایی می‌شود (بجای نام پیش فرض کلاس):
 <list-group></list-group>
همچنین در اینجا، یک خاصیت عمومی نیز تعریف شده‌است. تمام خواص عمومی تعریف شده‌ی در اینجا به صورت ویژگی‌هایی در تگ نهایی TagHelper قابل دسترسی و مقدار دهی خواهند بود:
 <list-group asp-items="Model.Items"></list-group>
 برای لغو این حالت می‌توان از ویژگی HtmlAttributeNotBound استفاده کرد.
برای اینکه نام این ویژگی را نیز بتوانیم سفارشی سازی کنیم، می‌توان از ویژگی HtmlAttributeName استفاده کرد. در صورت عدم ذکر آن، از نام پیش فرض این خاصیت عمومی جهت تعریف ویژگی‌های تگ نهایی استفاده می‌گردد.
عملیات نهایی افزودن تگ‌های HTML، به View برنامه، در متد Process انجام می‌شود. در اینجا توسط متدهایی مانند output.Content.AppendHtml می‌توان خروجی دلخواهی را به صفحه اضافه کرد.


تکمیل کدهای Tag Helper سفارشی رندر کردن لیست‌های بوت استرپی

پس از آشنایی با ساختار کلی یک کلاس TagHelper، اکنون می‌توان کدهای آن را به نحو ذیل تکمیل کرد:
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
 
namespace Core1RtmEmptyTest.TagHelpers
{
    [HtmlTargetElement("list-group")]
    public class ListGroupTagHelper : TagHelper
    {
        [HtmlAttributeName("asp-items")]
        public List<string> Items { get; set; }
 
        protected HttpRequest Request => ViewContext.HttpContext.Request;
 
        [ViewContext, HtmlAttributeNotBound]
        public ViewContext ViewContext { get; set; }
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
 
            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }
 
            if (Items == null)
            {
                throw new InvalidOperationException($"{nameof(Items)} must be provided");
            }
 
            output.TagName = "ul";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Attributes.Add("class", "list-group");
 
            foreach (var item in Items)
            {
                TagBuilder itemBuilder = new TagBuilder("li");
                itemBuilder.AddCssClass("list-group-item");
                itemBuilder.InnerHtml.Append(item);
                output.Content.AppendHtml(itemBuilder);
            }
        }
    }
}
توضیحات:
 - چون می‌خواهیم تگ نهایی آن، list-group نام داشته باشد، آن‌را توسط ویژگی HtmlTargetElement به صورت صریحی مشخص کرده‌ایم.
 - همچنین علاقمندیم تا ویژگی دریافت لیست آیتم‌ها، نامی معادل asp-items داشته باشد. بنابراین آن‌را نیز توسط ویژگی HtmlAttributeName، دقیقا مشخص کرده‌ایم.
 - در این کلاس، یک خاصیت اضافه‌ی ViewContext را نیز مشاهده می‌کنید. ویژگی ViewContext اعمالی به آن، سبب خواهد شد تا اطلاعات درخواست جاری، به این خاصیت عمومی، به صورت خودکار تزریق شود. بنابراین اگر نیاز به اطلاعاتی مانند Request جاری دارید، شیء ViewContext.HttpContext.Request، این مقادیر را در اختیار شما قرار می‌دهد. به علاوه اگر دقت کرده باشید، این خاصیت با ویژگی HtmlAttributeNotBound مزین شده‌است. از این جهت که نمی‌خواهیم این خاصیت عمومی، در لیست ویژگی‌های تگ نهایی TagHelper در حال تهیه، ظاهر شود.
 - پس از آن کاری که انجام شده، تکمیل متد Process است. در اینجا توسط output.TagName مشخص می‌کنیم که TagHelper جاری، در بین تگ‌های ul قرار گیرد (مفهوم TagMode.StartTagAndEndTag ذکر شده) و همچنین این تگ محصور کننده دارای کلاس list-group بوت استرپ نیز خواهد بود.
 - سپس بر روی لیست آیتم‌های دریافت شده، یک حلقه را تشکیل داده و به کمک TagBuilder، تگ‌های li داخل ul برونی را تکمیل می‌کنیم. این TagBuilder در نهایت توسط متد output.Content.AppendHtml به View برنامه اضافه خواهد شد. در اینجا، متد Append هم وجود دارد. اگر از آن استفاده شود، خروجی نهایی HTML Encoded خواهد بود.
 

ثبت و استفاده‌ی از TagHelper سفارشی تهیه شده

پس از تعریف TagHelper سفارشی فوق، ابتدا ارجاعی از اسمبلی آن‌را به پروژه‌ی جاری اضافه می‌کنیم و سپس به فایل ViewImports.cshtml_ برنامه مراجعه و یک سطر ذیل را به آن اضافه می‌کنیم:
 @addTagHelper *,Core1RtmEmptyTest.TagHelpers
در اینجا عبارت Core1RtmEmptyTest.TagHelpers همان نام فضای نام اصلی پروژه‌ی Class library دربرگیرنده‌ی TagHelper سفارشی است.
اکنون که این TagHelper در Viewهای برنامه قابل شناسایی است، روش افزودن آن، بر اساس همان سفارشی سازی‌هایی است که انجام دادیم:

نظرات مطالب
ASP.NET MVC #11
یکی از اون مسائلی که در ابتدای  بررسی mvc واسه من مساله بود این بود که چطور  صفحه ای که از چند بخش تشکیل شده باشه رو با یه مدل باید ساخت .تا اینکه به viewModel رسیدم و به نظرم پاسخگوی مساله بود.
من اینطور فهمیده بودم که viewModel فقط برای strongly type بودن view هستش و هیچ منطقی قرار نیست در اون پیاده بشه. بنابراین اگه چند بخش مختلف مثل اخبار، نفرات حاضر و ... بوسیله اون نمایش داده بشه مشکلی ایجاد نمی کنه. چون ViewModel به فرض فقط لیستی از خبرها رو نگهداری میکنه و پر کردن اون لیست وظیفه اش نیست.
ولی مثل اینکه دوستان نظر دیگه ای دارن.
نظرات مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
- در مورد صفحه بندی اطلاعات در MVC، مطلب زیاد وجود دارد.
- در برنامه‌های MVC عموما از افزونه‌های گریدهای مبتنی بر jQuery برای اینکار استفاده می‌شود. مانند kendo ui و یا jqGrid و یا DataTables و ... (این‌ها مثال‌های NET Core. هم دارند: ^ و ^) و یا «پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery» و یا WebGrid