۱۳۹۱/۰۱/۰۸

ASP.NET MVC #4


بررسي نحوه ارتباطات بين اجزاي مختلف الگوي MVC در ASP.NET MVC

اينبار برخلاف قسمت قبل، قالب پروژه خالي ASP.NET MVC را در VS.NET انتخاب كرده (ASP.NET MVC 3 Web Application و بعد انتخاب قالب Empty نمايش داده شده) و سپس پروژه جديدي را شروع مي‌كنيم. View Engine را هم Razor در نظر خواهيم گرفت.
پس از ايجاد ساختار اوليه پروژه، بدون اعمال هيچ تغييري، برنامه را اجرا كنيد. بلافاصله با پيغام The resource cannot be found يا 404 يافت نشد، مواجه خواهيم شد.
همانطور كه در پايان قسمت دوم نيز ذكر شد، پردازش‌ها در ASP.NET MVC از كنترلرها شروع مي‌شوند و نه از صفحات وب. بنابراين براي رفع اين مشكل نياز است تا يك كلاس كنترلر جديد را اضافه كنيم. به همين جهت بر روي پوشه استاندارد Controllers كليك راست كرده، از منوي ظاهر شده قسمت Add، گزينه‌ي Controller را انتخاب كنيد:


در صفحه بعدي كه ظاهر مي‌شود، نام HomeController را وارد كنيد (با توجه به اينكه مطابق قراردادهاي ASP.NET MVC، نام كنترلر بايد به كلمه Controller ختم شود). البته لازم به ذكر است كه اين مراحل را به همان شكل متداول مراجعه به منوي Project و انتخاب Add Class و سپس ارث بري از كلاس Controller نيز مي‌توان انجام داد و طي اين مراحل الزامي نيست. كلاسي كه به صورت خودكار از طريق منوي Add Controller ياد شده ايجاد مي‌شود، به شكل زير است:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/
        public ActionResult Index()
        {
            return View();
        }
    }
}

سؤال:
در قسمت دوم عنوان شد كه كنترلر بايد كلاسي باشد كه اينترفيس IController را پياده سازي كرده است، اما در اينجا ارث بري از كلاس Controller را شاهد هستيم. جريان چيست؟!
سلسله مراتبي كه بكارگرفته شده به صورت زير است:
public abstract class ControllerBase : IController
public abstract class Controller : ControllerBase

ControllerBase، اينترفيس IController را پياده سازي كرده و سپس كلاس Controller از كلاس ControllerBase مشتق شده است. شايد بپرسيد كه اين همه پيچ و تاب براي چيست؟!
مشكلي كه با اينترفيس خالص وجود دارد، عدم نگارش پذيري آن است. به اين معنا كه اگر متدي يا خاصيتي در نگارش بعدي به اين اينترفيس اضافه شد، هيچكدام از پروژه‌هاي قديمي ديگر كامپايل نخواهند شد و بايد ابتدا اين متد يا خاصيت جديد را نيز لحاظ كنند. اينجا است كه كار كلاس‌هاي abstract شروع مي‌شود. در يك كلاس abstract مي‌توان پياده سازي پيش فرضي را نيز ارائه داد. به اين ترتيب مصرف كننده نهايي كلاس Controller متوجه اين تغييرات نخواهد شد.
اگر برنامه نويس «من» باشم، شما رو وادار خواهم كرد كه متدهاي جديد اينترفيس تعريفي‌ام را پياده سازي كنيد! همينه كه هست! اما اگر طراح مايكروسافت باشد، بلافاصله انبوهي از جماعت ايرادگير كه بالاي 100 تا از كنترلر‌هاي اون‌ها الان فقط در يك پروژه از كار افتاده،‌ ممكن است جلوي دفتر مايكروسافت دست به خود سوزي بزنند! اينجا است كه مايكروسافت مجبور است تا اين پيچ و تاب‌ها را اعمال كند كه اگر روزي متدي در اينترفيس IController بنابر نيازهاي جديد درنظر گرفته شد، بتوان سريع يك پياده سازي پيش فرض از آن‌را در كلاس‌هاي abstract ياد شده قرار داد (يكي از تفاوت‌هاي مهم كلاس‌هاي abstract با اينترفيس‌ها) تا جماعت ايرادگير و نق‌زن متوجه تغييري نشوند و باز هم پروژه‌هاي قديمي بدون مشكل كامپايل شوند. تابحال به فلسفه وجودي كلاس‌هاي abstract از اين ديدگاه فكر كرده بوديد؟!

بعد از اين توضيحات و كارها، اگر اينبار برنامه را اجرا كنيم، خطاي زير نمايش داده مي‌شود:

The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Home/Index.aspx
~/Views/Home/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/Home/Index.cshtml
~/Views/Home/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

همانطور كه ملاحظه مي‌كنيد چون نام كنترلر ما Home است، فريم ورك در پوشه استاندارد Views در زير پوشه‌اي به همان نام Home، به دنبال يك سري فايل مي‌گردد. فايل‌هاي aspx مربوط به View Engine ايي به همين نام بوده و فايل‌هاي cshtml و vbhtml مربوط به View Engine ديگري به نام Razor هستند.
بنابراين نياز است تا يكي از اين فايل‌ها را در مكان‌هاي ياد شده ايجاد كنيم. براي اين منظور حداقل دو راه وجود دارد. يا دستي اينكار را انجام دهيم يا اينكه از ابزار توكار خود VS.NET براي ايجاد يك View جديد استفاده كنيم.
براي ايجاد View ايي مرتبط با متد Index (در ASP.NET MVC نام ديگر متدهاي قرار گرفته در يك كنترلر، Action نيز مي‌باشد)، روي خود متد كليك راست كرده و گزينه Add View را انتخاب كنيد:


در صفحه بعدي ظاهر شده، پيش فرض‌ها را پذيرفته و بر روي دكمه Add كليك نمائيد. اتفاقي كه رخ خواهد داد شامل ايجاد فايل Index.cshtml، با محتواي زير است (با توجه به اينكه زبان پروژه سي شارپ است و View Engine انتخابي Razor مي‌باشد، cshtml توليد گرديد و گرنه vbhtml ايجاد مي‌شد):

@{
    ViewBag.Title = "Index";
}
<h2>Index</h2>

مجددا برنامه را اجرا كنيد. اينبار بدون خطايي كلمه Index را در صفحه توليدي مي‌توان مشاهده كرد. نكته جالب اين فايل‌هاي View جديد، عدم مشاهده ويژگي‌هاي runat=server و ساير موارد مشابه است.

چند سؤال مهم:
در حين ايجاد اولين كنترلر جهت نمايش صفحه پيش فرض برنامه، نام HomeController انتخاب شد. چرا مثلا نام TestController وارد نشد؟ برنامه از كجا متوجه شد كه بايد حتما اين كنترلر را پردازش كند. نقش متد Index چيست؟ آيا حتما بايد Index باشد و در اينجا نام ديگري را نمي‌توان وارد كرد؟ «قرارداد» پردازشي اين‌ها كجا تنظيم مي‌شود؟ فريم ورك، اين سيم كشي‌ها را چگونه انجام مي‌دهد؟
پاسخ به تمام اين سؤال‌ها، در ويژگي «مسيريابي يا Routing» نهفته است. فايل Global.asax.cs برنامه را باز كنيد. تعاريف مرتبط با مسيريابي پيش فرض را در متد RegisterRoutes آن مي‌توان مشاهده كرد:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
}



پروسه هدايت يك درخواست HTTP به يك كنترلر، در اينجا مسيريابي يا Routing ناميده مي‌شود. اين قابليت در فضاي نام System.Web.Routing تعريف شده است و بايد دقت داشت كه جزو ASP.NET MVC نيست. اين امكانات جزو ASP.NET Runtime است و به همراه دات نت 3.5 سرويس پك يك براي اولين بار ارائه شد. بنابراين جهت استفاده در ASP.NET Web forms نيز مهيا است. در ASP.NET MVC از اين امكانات براي ارسال درخواست‌ها به كنترلرها استفاده مي‌شود.
در متد routes.MapRoute فراخواني شده‌اي كه در كدهاي بالا ملاحظه مي‌كنيد، كار نگاشت يك URL به Actionهاي يك كنترلر صورت مي‌گيرد (يا همان متدهاي تعريف شده در كنترلرها). همچنين از اين URLها پارامترهاي اين متدها يا اكشن‌ها نيز قابل استخراج است.
در متد MapRoute، اولين پارامتر تعريف شده، يك نام پيش فرض است و در ادامه اگر آدرسي را به فرم «يك چيزي اسلش يك چيزي اسلش يك چيزي» يافت، اولين قسمت آن‌را به عنوان نام كنترلر تفسير خواهد كرد، دومين قسمت آن، نام متد عمومي موجود در كنترلر فرض شده و سومين قسمت به عنوان پارامتر ارسالي به اين متد پردازش مي‌شود.
براي مثال از آدرس زير اينطور مي‌توان دريافت كه:
http://hostname/home/about

Home نام كنترلي است كه فريم ورك به دنبال آن خواهد گشت تا اين درخواست رسيده را پردازش كند و about نام متدي عمومي در اين كلاس است كه به صورت خودكار فراخواني مي‌گردد. در اينجا پارامتر id ايي هم وجود ندارد. در يك برنامه امكان تعريف چندين و چند مسيريابي وجود دارد.
پارامتر سوم متد routes.MapRoute، يك سري پيش فرض را تعريف مي‌كند و اين مورد همانجايي است كه از اطلاعات آن جهت تعريف كنترلر پيش فرض استفاده كرديم. براي مثال به چهار آدرس زير دقت نمائيد:

http://localhost/
http://localhost/home
http://localhost/home/about
http://localhost/process/list

در حالت http://localhost/،‌ هر سه مقدار پيش فرض مورد استفاده قرار خواهند گرفت چون سه جزئي ({controller}/{action}/{id}) كه موتور مسيريابي به دنبال آن‌ها مي‌گردد، در اين آدرس وجود خارجي ندارد. بنابراين نام كنترلر پيش فرض در اين حالت همان Home مشخص شده در پارامتر سوم متد routes.MapRoute خواهد بود و متد پيش فرض نيز Index و پارامتري هم به آن ارسال نخواهد شد.
در حالت http://localhost/home نام كنترلر مشخص است اما دو جزء ديگر ذكر نشده‌اند، بنابراين مقادير آن‌ها از پيش فرض‌هاي صريح ذكر شده در متد routes.MapRoute گرفته مي‌شود. يعني نام متد اكشن مورد پردازش، Index خواهد بود به همراه هيچ آرگومان خاصي.
به علاوه بايد خاطرنشان كرد كه اين مقادير case sensitive يا حساس به بزرگي و كوچكي حروف نيستند. بنابراين مهم نيست كه http://localhost/HoMe باشد يا http://localhost/HOMe يا هر تركيب ديگري.
يا اگر آدرس رسيده http://localhost/process/list باشد، اين مسيريابي پيش فرض تعريف شده قادر به پردازش آن مي‌باشد. به اين معنا كه كنترلري به نام Process وهله سازي و سپس متدي به نام List در آن فراخواني خواهند شد.

يك نكته:
ترتيب routes.MapRouteهاي تعريف شده در اينجا مهم است و اگر اولين URL رسيده با الگوي تعريف شده مطابقت داشت، كار را تمام خواهد كرد و به ساير تعاريف نخواهد رسيد. مثلا اگر در اينجا يك مسيريابي ديگر را به نام Process تعريف كنيم:

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

    routes.MapRoute(
                "Process", // Route name
                "Process/{action}/{id}", // URL with parameters
                new { controller = "Process", action = "List", id = UrlParameter.Optional } // Parameter defaults
            );

    routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
}

آدرسي به فرم http://localhost/Process، به صورت خودكار به كنترلر Process و متد عمومي List آن نگاشت خواهد شد و كار به مسيريابي بعدي نخواهد رسيد.