اضافه کردن امکان آپلود چندگانه و مدیریت خطاها در ASP.NET MVC

ASP.NET MVC

همان طور که می دانید این مقاله در ادامه سری آموزشی “آموزش ASP.NET MVC در ۷  روز” است. امیدوارم که در این ۵ روز از آموزش لذت برده باشید و به خوبی تمرین کرده باشید.در این روز یه سری به آپلود فایل و خطاها در ASP.NET MVC زدیم.

نینجای MVC به روز ششم رسید.

فهرست:

  • تمرین بیست و ششم – اضافه کردن امکان آپلود چندگانه
    • پرسش و پاسخ در تمرین بیست و ششم
  • مشکل راه حل بالا
    • راه حل
  • تمرین بیست و هفتم – حل مشکل Thread Starvation
  • تمرین بیست و هشتم – مدیریت خطا، نمایش صفحه سفارشی پیغام خطا(Custom Error Page)
    • پرسش و پاسخ در تمرین بیست و هشتم
  • بررسی محدودیت تمرین بالا
  • تمرین بیست و نهم – مدیریت خطا، Log Exception
    • پرسش و پاسخ در تمرین بیست و نهم
  • Routing
    • معرفی RouteTable
    • بررسی چرخه درخواست در Net MVC
  • تمرین سی ام – پیاده سازی URLهای User Friendly
    • پرسش و پاسخ در تمرین سی ام
  • جمع بندی

تمرین بیست و ششم – اضافه کردن امکان آپلود چندگانه

در این تمرین، می خواهیم امکان آپلود چندین کارمند را از فایل CSV ایجاد کنیم.

در این تمرین دو مورد جدید یاد خواهیم گرفت:

  1. چگونگی استفاده از کنترل File Upload
  2. Controller های آسنکرون (نامتقارن)

گام ۱: ایجاد FileUploadViewModel

کلاس جدیدی با نام FileUploadViewModel مانند زیر در فولدر ViewModel ایجاد می کنیم.

<pre class="lang:c# decode:true ">public class FileUploadViewModel: BaseViewModel

{

    public HttpPostedFileBase fileUpload {get; set ;}

}

HttpPostedFieldBase امکان دسترسی به فایل آپلود شده توسط کاربر را فراهم می کند.

گام ۲: ایجاد BulkUploadController و index Action

Controller جدیدی با نام BulkUploadController و یک action method به نام Index به صورت زیر ایجاد می کنیم.

public class BulkUploadController : Controller
{
        [HeaderFooterFilter]
        [AdminFilter]
        public ActionResult Index()
        {
            return View(new FileUploadViewModel());
        } 
}

 

همان طور که مشاهده می کنید، صفت های HeaderFooterFilter و AdminFilter به Index اضافه شده است. HeaderFooterFilter برای این است که مطمئن شویم Header و داده های Footer به درستی به ViewModel ارسال شده باشند و AdminFilter دسترسی به action method را برای کاربران غیر Admin محدود می کند.

گام ۳: ایجاد Upload View

یک View برای action method بالا ایجاد می کنیم.

همان طور که تا این جا عمل کردیم، نام View باید index.cshtml باشد و در فولدر “~/Views/BulkUpload” قرار می گیرد.

گام ۴: طراحی Upload View

محتوای زیر را در آن قرار می دهیم:

 

@using WebApplication1.ViewModels
@model FileUploadViewModel
@{
    Layout = "~/Views/Shared/MyLayout.cshtml";
}

@section TitleSection{
    آپلود چندگانه
}
@section ContentBody{
    <div> 
    <a href="/Employee/Index">بازگشت</a>
        <form action="/BulkUpload/Upload" method="post" enctype="multipart/form-data">
            فایل را انتخاب کنید :<input type="file" name="fileUpload" value="" />
            <input type="submit" name="name" value="آپلود" />
        </form>
    </div>
}

 

همان طور که مشاهده می کنید، نام Property در FileUploadViewModel و نام کنترل ورودی [type=”file”] یکی هستند. درباره اهمیت نام گذاری صفات، پیش از این در تمرین Model Binder صحبت کردیم. نکته: در تگ فرم یک صفت اضافی که enctype است، مشخص شده است. در پایان این تمرین به آن می پردازیم.

گام ۵: ایجاد متد Business Layer Upload

در کلاس EmployeeBusinessLayer متد جدیدی با نام UploadEmployees ایجاد می کنیم.

public void UploadEmployees(List<Employee> employees)
{
    SalesERPDAL salesDal = new SalesERPDAL();
    salesDal.Employees.AddRange(employees);
    salesDal.SaveChanges();
}

 

گام ۶: ایجاد Upload Action Method

یک action method جدید با نام Upload به صورت زیر در BulkUploadController ایجاد می کنیم.

[AdminFilter]
public ActionResult Upload(FileUploadViewModel model)
{
    List<Employee> employees = GetEmployees(model);
    EmployeeBusinessLayer bal = new EmployeeBusinessLayer();
    bal.UploadEmployees(employees);
    return RedirectToAction("Index","Employee");
}

private List<Employee> GetEmployees(FileUploadViewModel model)
{
    List<Employee> employees = new List<Employee>();
    StreamReader csvreader = new StreamReader(model.fileUpload.InputStream);
    csvreader.ReadLine(); // Assuming first line is header
    while (!csvreader.EndOfStream)
    {
        var line = csvreader.ReadLine();
        var values = line.Split(',');//Values are comma separated
        Employee e = new Employee();
        e.FirstName = values[0];
        e.LastName = values[1];
        e.Salary = int.Parse(values[2]);
        employees.Add(e);
    }
    return employees;
}

 

AdminFilter اضافه شده، دسترسی کاربر غیر Admin را به متد Upload  محدود می کند.

گام ۷: ایجاد لینک برای BulkUpload

فایل AddNewLink.cshtml را از فولدر ” Views/Employee” باز کرده و مانند زیر لینکی برای BulkUpload قرار می دهیم:

<a href="/Employee/AddNew">کارمند جدید</a>
&nbsp;
&nbsp;
<a href="/BulkUpload/Index">آپلود چندگانه</a>

 

گام ۸: تست و اجرا

گام ۸.۱ – ایجاد یک فایل ساده برای تست

فایل ساده ای مانند فایل زیر ایجاد کرده و آن را ذخیره می کنیم.

گام ۸.۲ – تست و اجرا

کلید F5 را فشار داده و برنامه را اجرا می کنیم. فرایند ورود را کامل کرده و با کلیک روی “آپلود چندگانه” به این صفحه می رویم.

فایل را انتخاب کرده و روی دکمه آپلود کلیک می کنیم.

نکات:

در مثال بالا، هیچ اعتبارسنجی سمت سرور و کلاینت در View اعمال نکردیم. این مسئله ممکن است باعث خطاهای زیر شود:

"Validation failed for one or more entities. See ‘EntityValidationErrors’ property for more details."

 

برای اینکه دلیل اصلی این خطا را بیابیم، به سادگی یک watch با استفاده از watch expression زیر برای زمانی که exception رخ می دهد، ایجاد می کنیم.

 (System.Data.Entity.Validation.DbEntityValidationException)$exception).EntityValidationErrors

 

Watch expression ذکر شده، ‘$expression’ اسنثنا های رخ داده در context جاری را نمایش می دهد، حتی اگر این استثنا Catch نشده و به هیچ متغیری تخصیص داده نشده باشد.

پرسش و پاسخ در تمرین بیست و ششم:

چرا اینجا از اعتبارسنجی استفاده نکردیم؟

اضافه کردن اعتبارسنجی سمت کلاینت و سرور به عهده شما خوانندگان عزیز گذاشته شده است. در ادامه راهنمایی هایی برای شما خواهیم گذاشت.

  • برای اعتبارسنجی سمت سرور از Data Annotation استفاده می کنیم.
  • برای اعتبارسنجی سمت کلاینت نیز می توانید Data Annotation را به کاربرده و اعتبارسنجی jQuery unobtrusive را پیاده سازی نمایید.

واضح است که این بار باید صفات custom data را به طور دستی تنظیم نمایید چرا که برای file input متد آماده Html Helper نداریم.

نکته: اگر این قسمت را به درستی متوجه نشدید، پیشنهاد می کنیم دوباره به قسمت “پیاده سازی اعتبارسنجی سمت کلاینت در Login View” سری بزنید.

  • برای اعتبارسنجی سمت کلانت می توانید جاوا اسکریپت دلخواه خود را نوشته و آن را در رویداد کلیک دکمه فراخوانی نمایید. چندان مشکل نخواهد بود چرا که input file یک نوع کنترل ورودی است و مقدار آن در جاوا اسکریپت قابل بازیابی و اعتبارسنجی می باشد.

HttpPostedFileBase چیست؟

HttpPostedFileBase دسترسی به فایل آپلود شده توسط کاربر را فراهم می کند. Model Binder مقدار تمام propertyهای کلاس FileUploadViewModel را در طول درخواست Post به روزرسانی می کند. درحال حاضر، تنها یک property در FileUploadViewModel داریم و Model Binder آن را روی فایل آپلود شده توسط کاربر اعمال می کند.

آیا می توانیم چندین کنترل file input داشته باشیم؟

بله، به دو روش می توانیم آن را پیاده سازی کنیم.

  1. کنترل های file input را ایجاد می کنیم. هر کنترلی باید نام مخصوص به خود را داشته باشد. حالا در کلاس FileUploadViewModel یک property از نوع HttpPostedFileBase برای هر یک از آن ها ایجاد می کنیم. نام هر property باید با نام آن کنترل یکسان باشد. بقیه کارها توسط Model Binder انجام می شود.
  2. کنترل های file input را ایجاد می کنیم. هر کنترلی باید نام مخصوص به خود را داشته باشد. حال به جای ایجاد چندین property از نوع HttpPostedFileBase، یک property از نوع لیست ایجاد می کنیم.

نکته: این مورد برای همه کنترل ها امکان پذیر است. زمانی که چندین کنترل با نام یکسان داشته باشید، اگر property یک پارامتر ساده باشد Model Binder  آن را با مقدار کنترل اول به روز رسانی می کند. اگر property یک لیست باشد، Model Binder مقادیر هر کنترل را در لیست قرار می دهد.

Enctype=”multipart/form-data” چه کاری انجام می دهد؟

خب چندان مهم نیست اما دانستن آن خالی از لطف هم نیست.

این صفت، نوع encodingای را که در زمان post داده ها استفاده می شود، مشخص می کند.

مقدار پیش فرض برای این صفت، “application/x-www-form-urlencoded” می باشد.

مثال: فرم ورود ما می تواند درخواست post زیر را به سرور ارسال کند.

POST /Authentication/DoLogin HTTP/1.1
Host: localhost:8870
Connection: keep-alive
Content-Length: 44
Content-Type: application/x-www-form-urlencoded
...
...
UserName=Admin&Passsword=Admin&BtnSubmi=Login

 

تمام مقادیر ورودی به عنوان یک بخش به صورت یک زوج کلید/مقدار متصل شده با & ارسال می شود.

زمانی که enctype=”multipart/form-data” به تگ فرم اضافه شود، درخواست post زیر به سرور ارسال خواهد شد.

POST /Authentication/DoLogin HTTP/1.1
Host: localhost:8870
Connection: keep-alive
Content-Length: 452
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarywHxplIF8cR8KNjeJ
...
...
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="UserName"

Admin
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="Password"

Admin
------WebKitFormBoundary7hciuLuSNglCR8WC
Content-Disposition: form-data; name="BtnSubmi"

Login
------WebKitFormBoundary7hciuLuSNglCR8WC--

 

همان طور که مشاهده می کنید، فرم در چندین بخش post می شود. هر بخش با یک boundary(مرز) و Content-Type از هم جدا  می شوند و هر بخش شامل یک مقدار است.

اگر تگ فرم شامل کنترل file input باشد، encType باید به صورت “multipart/form-data” تعریف شود.

نکته: boundary در هر درخواست به طور تصادفی تولید می شود و ممکن است boundary های مختلفی را مشاهده نمایید.

جرا ما همیشه encType را از نوع “multipart/form-data” تعریف نمی کنیم؟

زمانی که encType از نوع “multipart/form-data” تعریف می شود، دو کار انجام می دهد داده ها را post می کند و فایل را آپلود می کند. به همین دلیل همیشه از آن استفاده نمی کنیم، چرا که به این ترتیب سایز کلی درخواست افزایش می یابد. همان طور که می دانید سایز بیشتر درخواست مساوی با کارایی کمتر است. از این رو بهترین راهکار این است که آن را روی حالت پیش فرض یعنی “application/x-www-form-urlencoded” تنظیم کنیم.

چرا در اینجا ViewModel ایجاد کردیم؟

ما تنها یک Controller در View خود داشتیم. می توانستیم به جای ایجاد یک ViewModel جداگانه به طور مستقیم با اضافه کردن یک پارامتر از نوع HttpPostedFileBase با نام fileupload در متد Upload به همین نتیجه برسیم.

به کد زیر توجه نمایید:

public ActionResult Upload(HttpPostedFileBase fileUpload)
{
}

 

اما چرا ما یک کلاس جداگانه ایجاد کردیم.

ایجاد ViewModel بهترین راهکار است. همیشه Controller باید داده ها را به شکل ViewModel به View ارسال کند و داده های ارسال شده از View باید به صورت ViewModel به Controller برسند.

مشکل راه حل بالا:

آیا تا به حال فکر کرده اید که چگونه زمانی که درخواستی ارسال می کنید، پاسخ را دریافت می نمایید؟

لطفا نگید که action method درخواست را دریافت می کند و… و…

اگرچه این جواب درست است اما انتظار پاسخی متفاوت داریم.

در ابتدا، سوال ما این است که چه اتفاقی می افتد؟

یک قانون ساده برنامه نویسی – همه چیز در یک برنامه توسط یک Thread اجرا می شود حتی یک درخواست!

درمورد ASP.Net در یک وب سرور، .net framework مخزنی (pool) از Threadها را نگه داری می کند.

زمانی که یک درخواست به وب سرور ارسال می شود، یک Thread آزاد از مخزن برای رسیدگی به درخواست گرفته می شود. این Thread، به عنوان worker  thread شناخته می شود.

 

زمانی که درخواست پاسخ داده می شود، این worker thread بلاک می شود و نمی تواند به درخواست دیگری رسیدگی کند.

حال فرض کنید، یک برنامه درخواست های بسیاری دریافت نماید و هر درخواست زمان زیادی را برای پردازش کامل نیاز داشته باشد. در این مورد، ممکن است ما با حالتی مواجه شویم که درخواست جدیدی به سرور ارسال می شود و هیچ worker thread آزادی در دسترس نیست. به این مشکل Thread Starvation می گوییم.

در مثال ما، فایل نمونه فقط ۲ رکورد کارمند داشت اما در حالت واقعی ممکن است هزاران رکورد داشته باشیم. این به این معناست که ممکن است درخواست، زمان زیادی از سرور برای تکمیل پردازش بگیرد و باعث Thread Starvation شود.

راه حل:

درخواست هایی که تاکنون درباره آن ها بحث کردیم، همگی از نوع متقارن یا سنکرون بودند.

حال اگر به جای درخواست های سنکرون، کاربر درخواست های آسنکرون ارسال نماید مشکل Starvation حل خواهد شد.

  • درمورد درخواست آسنکرون، به طور معمول thread worker از مخزن thread برای بررسی درخواست گرفته می شود.
  • Worker thread عملیات آسنکرون را تعریف کرده و به مخزن thread برمی گرداند تا درخواست دیگری را رسیدگی نماید. حال عملیات آسنکرون با CLR thread ادامه پیدا می کند.
  • حال مشکل این است که، CLR Thread نمی تواند پاسخی برگرداند، بنابراین زمانی که عملیات آسنکرون را به پایان می رساند، به Net اطلاع می دهد.
  • وب سرور دوباره یک worker Thread گرفته و باقیمانده درخواست را پردازش کرده و پاسخ را برمی گرداند.

در این سناریو، worker thread دو بار از مخزن thread فراخوانده می شود. حال ممکن است هر دو بار یک Thread گرفته شود و یا خیر.

حال در مثال ما خواندن فایل یک عمل وابسته به I/O (I/o bound) می باشد که نیازی به پردازش توسط worker thread ندارد. بنابراین بهترین موقعیت برای تبدیل درخواست های سنکرون به آسنکرون می باشد.

آیا درخواست آسنکرون زمان پاسخ را بهبود می بخشد؟

خیر، زمان پاسخ به همان صورت باقی می ماند. در اینجا thread برای رسیدگی به درخواست های دیگر آزاد می شود.

تمرین بیست و هفتم – راه حل مشکل Thread Starvation

در ASP.Net MVC می توانیم با تبدیل متد سنکرون به متد آسنکرون درخواست های سنکرون را به درخواست های آسنکرون تبدیل کنیم.

گام ۱: ایجاد Controller آسنکرون

UploadController را به AsyncController تغییر می دهیم.

 

{
    public class BulkUploadController : AsyncController
    {

گام ۲: تبدیل متد سنکرون به متد آسنکرون

این کار به سادگی با کمک دو کلمه کلیدی انجام می شود: async و await

 

[AdminFilter]
public async Task<ActionResult> Upload(FileUploadViewModel model)
{
    int t1 = Thread.CurrentThread.ManagedThreadId;
    List<Employee> employees = await Task.Factory.StartNew<List<Employee>>
        (() => GetEmployees(model));
    int t2 = Thread.CurrentThread.ManagedThreadId;
    EmployeeBusinessLayer bal = new EmployeeBusinessLayer();
    bal.UploadEmployees(employees);
    return RedirectToAction("Index", "Employee");
}<actionresult><employee><list<employee>
</list<employee>

همان طور که مشاهده می کنید، Id مربوط به thread را در ابتدا و انتهای متد در یک متغیر ذخیره می کنیم.

اجازه دهید که نگاه دقیق تری روی کد داشته باشیم.

  • زمانی که دکمه “آپلود” توسط کاربر کلیک می شود، درخواست جدیدی به سرور ارسال می شود.
  • وب سرور یک worker thread از مخزن thread برای پاسخ به درخواست می گیرد.
  • Worker thread، متد را مجبور به اجرا می کند.
  • Worker method یک عملیات آسنکرون را به کمک متد Factory.StartNew شروع می کند.
  • همان طور که مشاهده می کنید، متد با کلمه کلیدی async به صورت آسنکرون مشخص شده است. به این صورت مطمئن می شویم به محض این که عملیات آسنکرون شروع شد، worker thread آزاد می شود. حال عملیات آسنکرون در پس زمینه(background) توسط یک CLR Thread ادامه پیدا می کند.
  • حال فراخواننده عملیات آسنکرون با کلمه کلیدی await مشخص شده است. با این کار مطمئن می شویم که خط بعدی اجرا نمی شود، مگر اینکه عملیات آسنکرون کامل شده باشد.
  • زمانی که عملیات آسنکرون کامل شد، دستور بعدی در متد باید اجرا شود و برای این کار دوباره worker thread نیاز است. بنابراین وب سرور به سادگی یک worker thread آزاد جدید برای رسیدگی به باقیمانده درخواست و ارسال پاسخ از مخزن thread می گیرد.

گام ۳: تست و اجرا

برنامه را اجرا کرده و به BulkUpload می رویم.

حال قبل از اینکه هیچ کاری در خروجی انجام دهیم، به سراغ کد رفته و یک breakpoint در خط آخر قرار می دهیم.

حال فایل نمونه را انتخاب کرده و روی دکمه آپلود کلیک می کنیم.

 

همان طور که مشاهده می کنید، Id Thread های متفاوتی در شروع و پایان کار داریم. خروجی برنامه مانند تصویر تمرین قبل خواهد بود.

تمرین بیست و هشتم – مدیریت خطا، نمایش صفحه سفارشی پیغام خطا

یک پروژه بدون مدیریت خطای مناسب، به هیچ عنوان پروژه کاملی محسوب نمی شود.

تا اینجا، ما درباره دو filter در ASP.Net MVC صحبت کردیم – Action Filter و Authorization Filter. حالا نوبت به سومین مورد می رسد، Exception Filter.

Exception Filter چیست؟

Exception Filter نیز درست مانند filter های دیگر استفاده می شود. آن ها را به صورت صفت استفاده می کنیم.

مراحل استفاده از exception filter:

  • فعال سازی آن ها
  • اضافه کردن آن ها به عنوان یک صفت به action method یا Controller. همچنین می توانیم exception filters را در سطح global اعمال کنیم.

این filter ها چه کاری انجام می دهند؟

به محض اینکه exceptionای در متد رخ دهد، این فیلتر کنترل اجرا را به دست می گیرد و به طور اتوماتیک شروع به اجرای کد درون خود می کند.

آیا می توان آن را به صورت خودکار در آورد؟

ASP.Net MVC یک exception filter آماده به نام HandleError برای ما فراهم کرده است.

همان طور که قبل از این گفتیم، این فیلتر زمانی اجرا می شود که در یک متد exceptionای رخ داده باشد. این filter یک View در “~/Views/[current controller]” یا فولدر “~/Views/Shared” با نام “Error” را پیدا می کند، یک ViewResult از آن View ایجاد کرده و آن را به عنوان پاسخ برمی گرداند.

اجازه دهید که مثال را ببینیم و عملکرد آن را بهتر درک کنیم.

در آخرین تمرین پروژه مان، امکان آپلود چندگانه را پیاده سازی کردیم. حالا احتمال بالایی در رخ دادن خطا در input file وجود دارد.

گام ۱: ایجاد یک فایل نمونه با خطا

یک فایل برای آپلود ایجاد می کنیم اما این بار یک داده نامعتبر در آن وارد می کنیم.

همان طور که می بینید، فیلد حقوق نامعتبر است.

گام ۲: اجرا و تست خطا

کلید F5 را فشار داده و برنامه را اجرا می کنیم. به BulkUpload رفته و فایل بالا را انتخاب می کنیم و روی دکمه “آپلود” کلیک می کنیم.

ASP.NET MVC

گام ۳: فعال سازی Exception Filter

Exception Filters زمانی فعال می شوند که custom exception فعال باشد. برای فعال سازی custom Exception، فایل Web.config را باز کرده و به قسمت System.Web می رویم. قسمت جدیدی را مانند شکل زیر برای custom error ایجاد می کنیم.

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

گام ۴: ایجاد Error View

در فولدر “~/Views/Shared” یک View با نام “Error.cshtml” می بینید. این فایل به عنوان بخشی از قالب MVC از همان ابتدا ایجاد شده است. حال اگر ایجاد نشده باشد، می توان آن را به صورت دستی ایجاد کرد.

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <hgroup>
        <h1>Error.</h1>
        <h2>An error occurred while processing your request.</h2>
    </hgroup>
</body>
</html>

گام ۵: اضافه کردن Exception Filter

همان طور که گفتیم، قبل از فعال سازی exception filter آن را به متد یا Controller اضافه می کنیم.

یک خبر خوب J نیازی نیست که آن را به صورت دستی اضافه کنیم.

فایل FilterConfig.cs را از فولدر App_Start باز می کنیم. در متد RegisterGlobalFilters می توانید این HandleError Filter را که در سطح Global اضافه شده است، ببینید.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());//ExceptionFilter
    filters.Add(new AuthorizeAttribute());
}

اگر لازم بود، Global Filter را حذف کرده و آن را در action یا Controller اضافه کنید.

[AdminFilter]
[HandleError]
public async Task<ActionResult> Upload(FileUploadViewModel model)
{<actionresult>
</actionresult>

البته این روش پیشنهاد نمی شود. بهتر است که آن را در سطح Global اعمال نمایید.

گام ۶: تست و اجرا

اجازه دهید، برنامه را به همان صورت قبلی تست کنیم.

 

گام ۷: نمایش پیغام خطا در View

برای دست یابی به این مورد، Error View را به Strong type از نوع کلاس HandleErrorInfo تبدیل می کنیم و سپس پیغام خطا را در View نمایش می دهیم.

 

@model HandleErrorInfo
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <hgroup>
        <h1>Error.</h1>
        <h2>An error occurred while processing your request.</h2>
    </hgroup>
        Error Message :@Model.Exception.Message<br />
        Controller: @Model.ControllerName<br />
        Action: @Model.ActionName
</body>
</html>

گام ۸: تست و اجرا

دوباره تست قبل را انجام می دهیم اما این بار Error View زیر را دریافت می کنیم.

آیا ما چیزی را فراموش کردیم؟

صفت HandleError این اطمینان را به وجود می آورد که این View سفارشی هر بار که exceptionای در متد رخ می دهد، نمایش داده می شود. اما قدرت آن به Controller و action method محدود است. این فیلتر نمی تواند خطای “Resource not found” را مدیریت کند.

برنامه را اجرا کرده و یک چیز عجیب و غریب و غیر معمول در URL تایپ کنید.

 

گام ۹: ایجاد ErrorController به صورت زیر

یک Controller جدید با نام ErrorController در فولدر Controller ایجاد می کنیم و یک متد Index نیز به صورت زیر در آن ایجاد می کنیم.

public class ErrorController : Controller
{
    // GET: Error
    public ActionResult Index()
    {
        Exception e=new Exception("Invalid Controller or/and Action Name");
        HandleErrorInfo eInfo = new HandleErrorInfo(e, "Unknown", "Unknown");
        return View("Error", eInfo);
    }
}

سازنده HandleErrorInfo سه آرگومان می گیرد – شیء Exception، نام Controller و نام action method

گام ۱۰: نمایش Custom Error View روی URL نامعتبر

در فایل web.config تنظیماتی برای خطای “Resource not found” مانند زیر تعریف می کنیم:

<system.web>
    <customErrors mode="On">
      <error statusCode="404" redirect="~/Error/Index"/>
    </customErrors>

گام ۱۱: امکان دسترسی به ErrorController برای همه

صفت AllowAnonymous را به ErrorController اضافه می کنیم چرا که Controller و action method نباید در دسترس کاربر غیرمجاز قرار بگیرند. ممکن است که کاربر قبل ورود به سایت، آدرس نامعتبر وارد کرده باشد.

[AllowAnonymous]
public class ErrorController : Controller
{

گام ۱۲: تست و اجرا

برنامه را اجرا کرده و یک URL نامعتبر در آدرس بار ایجاد می کنیم.

پرسش و پاسخ در تمرین بیست و هشتم:

آیا می توانیم نام View را تغییر دهیم؟

بله، نیازی نیست که نام View را همیشه Error بگذاریم.

در این مورد، می توانیم زمانی که HandleError filter  را اضافه می کنیم نام آن را مشخص کنیم.

 

[HandleError(View="MyError")]
Or
filters.Add(new HandleErrorAttribute()
                {
                    View="MyError"
                });

آیا می توان برای exceptionهای متفاوت، Error view های مختلف داشت؟

بله، در این حالت باید HandleError filter را چندین بار اعمال کنیم.

[HandleError(View="DivideError",ExceptionType=typeof(DivideByZeroException))]
[HandleError(View = "NotFiniteError", ExceptionType = typeof(NotFiniteNumberException))]
[HandleError]

OR

filters.Add(new HandleErrorAttribute()
    {
        ExceptionType = typeof(DivideByZeroException),
        View = "DivideError"
    });
filters.Add(new HandleErrorAttribute()
{
    ExceptionType = typeof(NotFiniteNumberException),
    View = "NotFiniteError"
});
filters.Add(new HandleErrorAttribute());

در مثال بالا، HandleError را سه بار اضافه کردیم. دو بار اول ۲ exception را مشخص می کنند در حالی که آخری به صورت عمومی است و برای همه exception های دیگر Error View را نمایش می دهد.

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

تنها محدودیت تمرین بالا این است که ما هرگز وارد exception  خود نشدیم و از آن آگاه نیستیم.

تمرین بیست و نهم – مدیریت خطا، Log Exception

گام ۱: ایجاد کلاس Logger

در ریشه پروژه، فولدری به نام Logger ایجاد می کنیم.

کلاس جدیدی با نام FileLogger به صورت زیر در این فولدر ایجاد می کنیم.

namespace WebApplication1.Logger
{
    public class FileLogger
    {
        public void LogException(Exception e)
        {
            File.WriteAllLines("C://Error//" + DateTime.Now.ToString("dd-MM-yyyy mm hh ss")+".txt", 
                new string[] 
                {
                    "Message:"+e.Message,
                    "Stacktrace:"+e.StackTrace
                });
        }
    }
}

گام ۲: ایجاد کلاس EmployeeExceptionFilter

کلاس جدیدی با نام EmployeeExceptionFilter در فولدر Filters ایجاد می کنیم.

 

namespace WebApplication1.Filters
{
    public class EmployeeExceptionFilter
    {
    }
}

گام ۳: گسترش HandleError برای پیاده سازی Logging

کلاس EmployeeExceptionFilter از HandleErrorAttribute ارث بری می کند و متد OnException را به صورت زیر پیاده سازی می کند:

 

public class EmployeeExceptionFilter:HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        base.OnException(filterContext);
    }
}

 

توجه داشته باشید که باید فضای نام System.Web.MVC را بالای کلاس قرار دهیم.

گام ۴: تعریف متد OnException

کد Exception logging را به صورت زیر در متد OnException قرار می دهیم.

public override void OnException(ExceptionContext filterContext)
{
    FileLogger logger = new FileLogger();
    logger.LogException(filterContext.Exception);
    base.OnException(filterContext);
}

 

گام ۵: تغییر Exception filter پیش فرض

فایل FilterConfig.cs را باز کرده و HandleErrorAttribute را حذف می کنیم و متدی را که در مرحله قبل ایجاد کردیم به آن اضافه می کنیم.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    //filters.Add(new HandleErrorAttribute());//ExceptionFilter
    filters.Add(new EmployeeExceptionFilter());
    filters.Add(new AuthorizeAttribute());
}

 

گام ۶: تست و اجرا

اول از همه یک فولدر به نام “Error” در درایو C ایجاد می کنیم، زیرا فایل خطاها قرار است در این مسیر قرار بگیرد.

نکته: اگر لازم است، مسیر آن را به مسیر دلخواه خود تغییر دهید.

کلید F5 را فشار داده و برنامه را اجرا کنید. به “آپلود چندگانه” رفته و فایل بالا را انتخاب کرده و روی دکمه “آپلود” کلیک می کنیم.

خروجی این بار متفاوت نخواهد بود، همان Error View قبلی را دوباره دریافت می کنیم. تنها تفاوت این است که یک فایل خطا نیز در مسیر “C:\\Errors” خواهیم داشت.

پرسش و پاسخ در تمرین بیست و نهم:

زمانی که exception رخ می دهد، Error View چگونه به عنوان پاسخ برگردانده می شود؟

در تمرین بالا، ما متد OnException را Override کرده و امکان exception logging را پیاده سازی کردیم. حال سوال این است، چگونه HandleError filter پیش فرض هنوز کار می کند؟ بسیار ساده است، خط آخر در متد OnException را بررسی کنید.

base.OnException(filterContext);

 

این خط به این معنی است که اجازه بده بقیه کار را کلاس پایه OnException انجام دهد و این کلاس ViewResult مربوط به ErrorView را برمی گرداند.

آیا می توانیم نتیجه دیگری را در OnException برگردانیم؟

بله، به کد زیر توجه کنید:

public override void OnException(ExceptionContext filterContext)
{
    FileLogger logger = new FileLogger();
    logger.LogException(filterContext.Exception);
    //base.OnException(filterContext);
    filterContext.ExceptionHandled = true;
    filterContext.Result = new ContentResult()
    {
        Content="Sorry for the Error"
    };
}

 

زمانی که می خواهیم پاسخ دلخواه را برگردانیم، اولین کاری که باید انجام دهیم، این است که موتور MVC را آگاه کنیم که ما Exception را به صورت دستی مدیریت می کنیم، بنابراین رفتار پیش فرض را که نمایش صفحه پیش فرض خطاست، نمایش ندهد. این کار با دستور زیر انجام می شود:

 

filterContext.ExceptionHandled = true

 

Routing:

تا اینجا درباره بسیاری از مفاهیم بحث کردیم، و به جز یک مورد اساسی و مهم به بسیاری از سوالات در MVC پاسخ مناسبی پیدا کردیم.

زمانی که کاربر درخواستی ایجاد می کند، دقیقا چه اتفاقی می افتد؟

خب جواب آن قطعا این است “Action method اجرا می شود”. اما سوال دقیق ما این است که Controller و Action Method چگونه برای درخواست یک URL شناسایی می شوند.

قبل از اینکه تمرین “پیاده سازی URLهای User Friendly” را شروع کنیم، اجازه دهید که جواب سوال بالا را پیدا کنیم.

معرفی RouteTable:

در Asp.Net MVC مفهومی به نام RouteTable وجود دارد که URL را برای هدایت در برنامه ذخیره می کند. به زبان ساده تر، مجموعه ای از الگوهای URL ممکن در برنامه را نگه می دارد.

به طور پیش فرض یک route به عنوان بخشی از قالب برنامه به آن اضافه خواهد شد. برای بررسی آن فایل Global.asax را باز می کنیم. در Application_Start می توانیم دستوری مانند زیر بیابیم:

RouteConfig.RegisterRoutes(RouteTable.Routes);

 

فایل RouteConfig.cs را نیز می توان در فولدر App_Start پیدا کرد که شامل بلاک کد زیر می باشد:

namespace WebApplication1
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

 

همان طور که مشاهده می کنید، با استفاده از متد  routes.MapRouteدر متد RegisterRoutes یک route پیش فرض تعریف شده است.

Route های تعریف شده در متد RegisterRoutes بعدا در چرخه درخواست ASP.Net MVC استفاده خواهند شد تا مشخص کنند که دقیقا کدام Controller و Action Method باید اجرا شوند.

اگر لازم باشد، با استفاده از تابع route.MapRoute، بیش از یک route ایجاد می کنیم. به طور غیرمستقیم، تعریف route به معنی ایجاد شیء Route می باشد.

تابع MapRoute، همچنین صفت RouteHandler را به شیء route اضافه می کند که در MVC به طور پیش فرض MVCRouteHandler می باشد.

بررسی چرخه درخواست ASP.Net MVC

قبل از شروع لازم است که بدانید در این قسمت نمی خواهیم، چرخه درخواست را به طور ۱۰۰%  توضیح دهیم. تنها به یک مورد مهم اشاره خواهیم کرد:

گام ۱: UrlRoutingModule

زمانی که کاربر درخواستی ایجاد می کند، در ابتدا از طریق شیء UrlRoutingModule ارسال می شود. UrlRoutingModule یک ماژول HTTP می باشد.

گام ۲: Routing

UrlRoutingModule اولین شیء Route منطبق با درخواست را از جدول route برمی دارد. حالا برای پیدا کردن URL درخواست، آن را با الگوی URL تعریف شده در Route مقایسه می کند.

فاکتورهای زیر در مقایسه در نظر گرفته می شوند:

  • تعداد پارامترهای URL درخواست ها (به جز نام دامنه) باید با الگوی URL تعریف شده در Route یکی باشد.

مثال:

  • پارامترهای اختیاری تعریف شده در الگوی URL

مثال:

 

  • پارامترهای استاتیک تعریف شده در پارامتر

گام ۳: ایجاد MVC Route Handler

زمانی که شیء Route انتخاب شد، UrlRoutingModule از طریق شیء Route به MvcRouteHandler دسترسی پیدا می کند.

گام ۴: ایجاد RouteData و RequestContext

شیء UrlRoutingModule، با استفاده از شیء Route، RouteData را ایجاد می کند که سپس برای ایجاد RequestContext استفاده می شود.

RouteData اطلاعات مربوط به route ها مانند نام Controller، نام action method و مقادیر پارامترهای route را مخفی سازی(encapsulate) می کند.

نام Controller

برای گرفتن نام Controller از URL درخواست شده باید قانون ساده زیر را دنبال کنیم.

“در الگوی URL، {controller} کلمه کلیدی است که نام Controller را مشخص می کند.”

مثال:

  • زمانی که الگوی URL {controller}/{action}/{id} باشد و URL درخواست به صورت http://localhost:8870/BulkUpload/Upload/5 باشد، BulkUpload نام Controller خواهد بود.
  • زمانی که الگوی URL {action}/{controller}/{id} باشد و URL درخواست شده به صورت http://localhost:8870/BulkUpload/Upload/5 باشد، Upload نام Controller خواهد بود.

نام Action Method:

برای گرفتن نام Action Method از URL درخواست شده باید قانون ساده زیر را دنبال کنیم.

“در الگوی URL، {Action Method} کلمه کلیدی است که نام Action Method را مشخص می کند.”

مثال:

  • زمانی که الگوی URL {controller}/{action}/{id} باشد و URL درخواست به صورت http://localhost:8870/BulkUpload/Upload/5 باشد، Upload نام Action Method خواهد بود.
  • زمانی که الگوی URL {action}/{controller}/{id} باشد و URL درخواست شده به صورت http://localhost:8870/BulkUpload/Upload/5 باشد، BulkUpload نام Action Method خواهد بود.

پارامترهای Route:

به طور معمول، یک الگوی URL می تواند شامل ۴ چیز باشد.

  1. {Controller} -> نام Controller را مشخص می کند.
  2. {action} -> نام Action Method را مشخص می کند.
  3. یک رشته -> به طور مثال: “MyCompany/{controller}/{action}” در این الگو رشته “MyCompany” اجباری می شود.
  4. {یک پارامتر} -> مثال: “{controller}/{action}/{id}” در این الگو “id” پارامتر route است. پارامتر route می تواند در URL در زمان درخواست مقدار بگیرد.

به مثال زیر توجه کنید:

الگوی Route -> “{controller}/{action}/{id}”

URL درخواست شده -> http://localhost:8870/BulkUpload/Upload/5

تست اول:

public class BulkUploadController : Controller

{

    public ActionResult Upload (string id)

    {

       //value of id will be 5 -> string 5

       ...

    }

}

 

تست دوم:

public class BulkUploadController : Controller

{

    public ActionResult Upload (int id)

    {

       //value of id will be 5 -> int 5

       ...

    }

}

 

تست سوم:

public class BulkUploadController : Controller

{

    public ActionResult Upload (string MyId)

    {

       //value of MyId will be null

       ...

    }

}

گام ۵: ایجاد MVCHandler

MvcRouteHandler با داده های ارسالی شیء RequestContext نمونه ای از MVCHandler ایجاد می کند.

گام ۶: ایجاد نمونه ای از Controller

MVCHandler با استفاده از ControllerFactory (به طور پیش فرض DefaultControllerFactory خواهد بود) نمونه ای از Controller ایجاد می کند.

گام ۷: اجرای متد

MVCHandler متد اجرای Controller را فراخوانی می کند. متد اجرا در کلاس Controller Base تعریف شده است.

گام ۸: فراخوانی Action Method

همراه هر Controller، یک شیء ControllerActionInvoker وجود دارد. درون متد اجرا، شی ControllerActionMethod متد مناسب را فراخوانی می کند.

گام ۹: نتیجه اجرا

Action Method ورودی کاربر را دریافت کرده و داده های مناسب پاسخ را آماده می کند و سپس نتیجه را با نوع return برمی گرداند. حالا این نوع return، می تواند ViewResult باشد، یا RedirectToRoute یا هر چیز دیگری باشد.

خب من یقین دارم که به خوبی مفاهیم Routing را درک کردید، بنابراین URLهای پروژه مان را با کمک Routing کاربر پسند تر می کنیم.

تمرین سی ام – پیاده سازی URLهای User Friendly

Route های اضافی دیگری به صورت زیر در متد RegisterRoutes اضافه می کنیم.

public static void RegisterRoutes(RouteCollection routes)

{

    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");



    routes.MapRoute(

    name: "Upload",

    url: "Employee/BulkUpload",

    defaults: new { controller = "BulkUpload", action = "Index" }

    );



    routes.MapRoute(

        name: "Default",

        url: "{controller}/{action}/{id}",

        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }

    );

}

 

همان طور که می بینید، حالا بیش از یک route تعریف شده داریم.

گام ۲: تغییر رفرنس های URL

فایل AddNewLink.cshtml را از فولدر “~/Views/Employee” و لینک BulkUpload را به صورت زیر تغییر می دهیم:

&nbsp;

<a href="/Employee/BulkUpload">آپلود چندگانه</a>

 

گام ۳: تست و اجرا

برنامه را اجرا می کنیم.

همان طور که مشاهده می کنید، URL به شکل “Controller/Action” می باشد و همچنین بسیار کاربرپسند تر است اما خروجی به همان شکل سابق می باشد.

پیشنهاد می کنیم که route های بیشتری اضافه کرده و URLهای دیگری را امتحان نمایید.

پرسش و پاسخ در تمرین سی ام:

آیا URL های قبلی هنوز کار می کنند؟

بله، به خوبی کار می کنند.

حالا Index Action در BulkUploadController به دو روش قابل دسترسی است:

  1. http://localhost:8870/Employee/BulkUpload
  2. http://localhost:8870/BulkUpload/Index

“id” در route پیش فرض چیست؟

به این Id پارامتر Route گفته می شود که میتواند مقادیری را از طریق URL بگیرد. به نوعی جایگزین Query String می باشد.

تفاوت بین پارامتر Route و Query String چیست؟

  • Query String محدودیت سایز دارد، در حالی که ما می توانیم هر چند تا که بخواهیم پارامتر Route تعریف کنیم.
  • ما نمی توانیم هیچ محدودیتی روی مقادیر Query String تعریف کنیم اما می توانیم این محدودیت ها را روی پارامترهای Route اعمال کنیم.
  • تعریف مقدار پیش فرض برای پارامترهای Route امکان پذیر است در صورتی که برای Query String امکان پذیر نیست.
  • Query String باعث بی نظمی ظاهری و بهم ریختگی در URL می شود، در حالی که پارامتر Route آن را مرتب نگه می دارد.

چگونه می توان محدودیت ها را روی پارامتر Route اعمال کرد؟

این کار را می توان با عبارات منظم (Regular expression) انجام داد.

مثال: به route زیر توجه نمایید.

routes.MapRoute(

    "MyRoute",

    "Employee/{EmpId}",

    new {controller=" Employee ", action="GetEmployeeById"},

    new { EmpId = @"\d+" }

 );

Action Method به شکل زیر خواهد بود:

public ActionResult GetEmployeeById(int EmpId)

{

   ...

}

 

حالا اگر کاربری درخواستی با URL http://…/Employee/1 یا http://…/Employee/111 ایجاد کند، action method اجرا خواهد شد اما اگر کسی درخواستی با URL http://…/Employee/Baran ایجاد نماید با پیغام خطای “Resource Not Found” مواجه می شود.

آیا لازم است که نام پارامتر را مانند پارامتر Route در Action Method نگه داریم؟

معمولا یک الگو با فقط یک Route ممکن است یک یا چند پارامتر route داشته باشد. برای شناسایی هر پارامتر route به طور مستقل باید نام پارامتر را با همان نام پارامتر route در Action Method نگه داشت.

آیا تعریف الگوی URL برای Action Method آسان تر است؟

می توانیم از صفات مبتنی بر routing برای این کار استفاده کنیم.

گام ۱: فعال سازی صفت مبتنی بر routing

در متد RegisterRoutes، خط زیر را بعد از دستور IgnoreRoute قرار می دهیم.

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");



routes.MapMvcAttributeRoutes();



routes.MapRoute(

...

 

گام ۲: تعریف الگوی route برای action method

بسیار ساده صفت Route را به Index action مربوط به EmployeeController اضافه می کنیم.

[Route("Employee/List")]

public ActionResult Index()

{

 

گام ۳: تست و اجرا

برنامه را اجرا کرده و فرایند ورود را کامل می کنیم.

همان طور که مشاهده می کنید، همان خروجی اما با URL کاربر پسند تری داریم.

آیا می توانیم پارامترهای Route را با صفت مبتنی بر routing انجام دهیم؟

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

[Route("Employee/List/{id}")]

publicActionResult Index (string id) { ... }

 

درمورد محدودیت ها در این مثال، چطور؟

بسیار آسانتر خواهد شد.

[Route(“Employee/List/{id:int}”)]

می توانیم محدودیت های زیر را داشته باشیم:
۱.{x:alpha} –  اعتبارسنجی رشته

  1. {x:bool} – اعتبارسنجی بولین
  2. {x:datetime} – Date Time اعتبارسنجی
  3. {x:decimal} – Decimal اعتبارسنجی
  4. {x:double} – اعتبارسنجی مقادیر ۶۴ بیتی اعشاری
  5. {x:float} – اعتبارسنجی مقادیر ۳۲ بیتی اعشاری
  6. {x:guid} – GUID اعتبارسنجی
  7. {x:length(6)} – اعتبارسنجی طول
  8. {x:length(1,20)} – اعتبارسنجی حداکثر و حداقل طول
  9. {x:long} – ۶۴ int اعتبارسنجی
  1. {x:max(10)} – int اعتبارسنجی بیشترین مقدار

۱۲.{x:maxlength(10)} – اعتبارسنجی حداکثر طور

  1. {x:min(10)} – int اعتبارسنجی کمترین مقدار

۱۴.{x:minlength(10)} – اعتبارسنجی حداقل طول

۱۵.{x:range(10,50)} – اعتبارسنجی بازه اعداد طبیعی

۱۶.{x:regex(SomeRegularExpression)} – اعتبارسنجی عبارات منظم

دستور IgnoreRoutes در متد RegisterRoutes چه کاری انجام می دهد؟

زمانی که می خواهیم از routing برای یک پسوند خاص استفاده نکنیم، از این دستور استفاه می شود. به عنوان بخشی از قالب MVC دستور زیر در RegisterRoutes نوشته می شود.

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

 

به این معنی که اگر کاربر درخواستی با یک پسوند “.axd” ایجاد کند، هیچ عمل  routingای انجام نخواهد شد. درخواست به طور مستقیم به منابع فیزیکی دسترسی خواهد داشت.

همچنین می توانیم به راحتی دستور IgnoreRoute خود را تعریف کنیم.

جمع بندی:

در روز ششم ما پروژه ای که روز اول تعریف کرده بودیم، کامل کردیم. امیداوریم که از این سری آموزشی لذت برده باشید.

صبر کنید!!! پس روز هفتم چی شد؟

در روز هفتم، یک Single Page Application با استفاده از MVC، jQuery و Ajax ایجاد خواهیم کرد. این آموزش بسیار جالب تر و با چالش های جذابی همراه خواهد بود.

با ما همراه باشید!

دانلود فایل PDF

فاطمه زکایی

فاطمه زکایی هستم. فارغ التحصیل کارشناسی مهندسی نرم افزار، مدت سه سال هست که در زمینه توسعه اپلیکیشن های تحت وب و اندروید و همچنین تولید محتوای تخصصی برنامه نویسی تحت وب و اندروید در مجموعه mspsoft در خدمت شما هستم.

نوشته‌های مرتبط

دیدگاه‌ها

*
*

این سایت از اکیسمت برای کاهش هرزنامه استفاده می کند. بیاموزید که چگونه اطلاعات دیدگاه های شما پردازش می‌شوند.

    یک برنامه نویس پاسخ

    سلام
    بازم ممنون از آموزش های خوب تون.

    خدا قوت.

      باران بزرگمهر پاسخ

      خواهش میکنم خوشحالم که استفاده کردید.

جشنواره فروش ویژه عید تا عید با تخفیف های باورنکردنی در ام اس پی سافتبزن بریم
+