۱۳۹۱/۰۱/۲۵

ASP.NET MVC #14


آشنايي با نحوه معرفي تعاريف طرحبندي سايت به كمك Razor

ممكن است يك سري از اصطلاحات را در قسمت‌هاي قبل مانند master page در لابلاي توضيحات ارائه شده، مشاهده كرده باشيد. اين نوع مفاهيم براي برنامه نويس‌هاي ASP.NET Web forms آشنا است (و اگر با Web forms view engine‌ در ASP.NET MVC كار كنيد، دقيقا يكي است؛ البته با اين تفاوت كه فايل code behind آن‌ها حذف شده است). به همين جهت در اين قسمت براي تكميل بحث، مروري خواهيم داشت بر نحوه‌ي معرفي جديد آن‌ها توسط Razor.
در يك پروژه جديد ASP.NET MVC و در پوشه Views\Shared\_Layout.cshtml آن، فايل Layout آن،‌ مفهوم master page را دارد. در اين نوع فايل‌ها، زير ساخت مشترك تمام صفحات سايت قرار مي‌گيرند:

<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>

<body>
    @RenderBody()
</body>
</html>

اگر دقت كرده باشيد، در هيچكدام از فايل‌هاي Viewايي كه تا اين قسمت به پروژه‌هاي مختلف اضافه كرديم، تگ‌هايي مانند body، title و امثال آن وجود نداشتند. در ASP.NET مرسوم است كليه اطلاعات تكراري صفحات مختلف سايت را مانند تگ‌هاي ياد شده به همراه منويي كه بايد در تمام صفحات قرار گيرد يا footer‌ مشترك بين تمام صفحات سايت، به يك فايل اصلي به نام master page كه در اينجا layout نام گرفته، Refactor كنند. به اين ترتيب حجم كدها و markup تكراري كه بايد در تمام Viewهاي سايت قرار گيرند به حداقل خواهد رسيد.
براي مثال محل قرار گيري تعاريف Content-Type تمام صفحات و همچنين favicon سايت، بهتر است در فايل layout باشد و نه در تك تك Viewهاي تعريف شده:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="shortcut icon" href="@Url.Content("~/favicon.ico")" type="image/x-icon" />


در كدهاي فوق يك نمونه پيش فرض فايل layout را مشاهده مي‌كنيد. در اينجا توسط متد RenderBody، محتواي رندر شده يك View درخواستي، به داخل تگ body تزريق خواهد شد.
تا اينجا در تمام مثال‌هاي قبلي اين سري، فايل layout در Viewهاي اضافه شده معرفي نشد. اما اگر برنامه را اجرا كنيم باز هم به نظر مي‌رسد كه فايل layout اعمال شده است. علت اين است كه در صورت عدم تعريف صريح layout در يك View، اين تعريف از فايل Views\_ViewStart.cshtml دريافت مي‌گردد:

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

فايل ViewStart، محل تعريف كدهاي تكراري است كه بايد پيش از اجراي هر View مقدار دهي يا اجرا شوند. براي مثال در اينجا مي‌شود بر اساس نوع مرورگر،‌ layout خاصي را به تمام Viewها اعمال كرد. مثلا يك layout‌ ويژه براي مرورگرهاي موبايل‌ها و layout ايي ديگر براي مرورگرهاي معمولي. امكان دسترسي به متغيرهاي تعريف شده در يك View در فايل ViewStart از طريق ViewContext.ViewData ميسر است.
ضمن اينكه بايد درنظر داشت كه مي‌توان فايل ViewStart را در زير پوشه‌هاي پوشه اصلي View نيز قرار داد. مثلا اگر فايل ViewStart ايي در پوشه Views/Home قرار گرفت، اين فايل محتواي ViewStart اصلي قرار گرفته در ريشه پوشه Views را بازنويسي خواهد كرد.
براي معرفي صريح فايل layout، تنها كافي است مسير كامل فايل layout را در يك View مشخص كنيم:

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Index</h2>

اهميت اين مساله هم در اينجا است كه يك سايت مي‌تواند چندين layout يا master page داشته باشد. براي نمونه يك layout براي صفحات ورود اطلاعات؛ يك layout خاص هم مثلا براي صفحات گزارش گيري نهايي سايت.
همانطور كه پيشتر نيز ذكر شد، در ASP.NET حرف ~ به معناي ريشه سايت است كه در اينجا ابتداي محل جستجوي فايل layout را مشخص مي‌كند.
به اين ترتيب زمانيكه يك كنترلر، View خاصي را فراخواني مي‌كند، كار از فايل Views\Shared\_Layout.cshtml شروع خواهد شد. سپس View درخواستي پردازش شده و محتواي نهايي آن، جايي كه متد RenderBody قرار دارد، تزريق خواهد شد.
همچنين مقدار ViewBag.Title ايي كه در فايل View تعريف شده، در فايل layout جهت رندر مقدار تگ title استفاده مي‌شود (انتقال يك متغير از View به يك فايل master page؛ كلاس layout، مدل View ايي را كه قرار است رندر كند به ارث مي‌برد).

يك نكته:
در نگارش سوم ASP.NET MVC امكان بكارگيري حرف ~ به صورت مستقيم در حين تعريف يك فايل js يا css وجود ندارد و حتما بايد از متد سمت سرور Url.Content كمك گرفت. در نگارش چهارم ASP.NET MVC، اين محدوديت برطرف شده و دقيقا همانند متغير Layout ايي كه در بالا مشاهده مي‌كنيد، مي‌توان بدون نياز به متد Url.Content، مستقيما از حرف ~ كمك گرفت و به صورت خودكار پردازش خواهد شد.


تزريق نواحي ويژه يك View در فايل layout

توسط متد RenderBody، كل محتواي View درخواستي در موقعيت تعريف شده آن در فايل Layout، رندر مي‌شود. اين ويژگي به نحو يكساني به تمام Viewها اعمال مي‌شود. اما اگر نياز باشد تا view بتواند محتواي markup قسمت ويژه‌اي از master page را مقدار دهي كند، مي‌توان از مفهومي به نام Sections استفاده كرد:
<!DOCTYPE html>
<html>
<head>
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
</head>
<body>
    <div id="menu">
        @if (IsSectionDefined("Menu"))
        {
            RenderSection("Menu", required: false);
        }
        else
        { 
            <span>This is the default ...!</span>   
        }
    </div>
    <div id="body">
        @RenderBody()
    </div>
</body>
</html>

در اينجا ابتدا بررسي مي‌شود كه آيا قسمتي به نام Menu در View جاري كه بايد رندر شود وجود دارد يا خير. اگر بله، توسط متد RenderSection، آن قسمت نمايش داده خواهد شد. در غيراينصورت، محتواي پيش فرضي را در صفحه قرار مي‌دهد. البته اگر از متد RenderSection با آرگومان required: false استفاده شود، درصورتيكه View جاري حاوي قسمتي به نام مثلا menu نباشد، تنها چيزي نمايش داده نخواهد شد. اگر اين آرگومان را حذف كنيم، يك استثناي عدم يافت شدن ناحيه يا قسمت مورد نظر صادر مي‌گردد.
نحوه‌ي تعريف يك Section در Viewهاي برنامه به شكل زير است:
@{
    ViewBag.Title = "Index";
    //Layout = null;
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>
    Index</h2>
@section Menu{
    <ul>
        <li>item 1</li>
        <li>item 2</li>
    </ul>
}

براي مثال فرض كنيد كه يكي از Viewهاي شما نياز به دو فايل اضافي جاوا اسكريپت براي اجراي صحيح خود دارد. مي‌توان تعاريف الحاق اين دو فايل را در قسمت header فايل layout قرار داد تا در تمام Viewها به صورت خودكار لحاظ شوند. يا اينكه يك section سفارشي را به نحو زير در آن View خاص تعريف مي‌كنيم:

@section JavaScript
{
   <script type="text/javascript" src="@Url.Content("~/Scripts/SomeScript.js")" />;
   <script type="text/javascript" src="@Url.Content("~/Scripts/AnotherScript.js")" />;
}

سپس كافي است جهت تزريق اين كدها به header تعريف شده در master page مورد نظر، يك سطر زير را اضافه كرد:

@RenderSection("JavaScript", required: false)

به اين ترتيب، اگر view ايي حاوي تعريف قسمت JavaScript نبود، به صورت خودكار شامل تعاريف الحاق اسكريپت‌هاي ياد شده نيز نخواهد گرديد. در نتيجه داراي حجمي كمتر و سرعت بارگذاري بالاتري نيز خواهد بود.



مديريت بهتر فايل‌ها و پوشه‌هاي يك برنامه ASP.NET MVC به كمك Areas

به كمك قابليتي به نام Areas مي‌توان يك برنامه بزرگ را به چندين قسمت كوچكتر تقسيم كرد. هر كدام از اين نواحي، داراي تعاريف مسيريابي، كنترلرها و Viewهاي خاص خودشان هستند. به اين ترتيب ديگر به يك برنامه‌ي از كنترل خارج شده ASP.NET MVC كه داراي يك پوشه Views به همراه صدها زير پوشه است، نخواهيم رسيد و كنترل اين نوع برنامه‌هاي بزرگ ساده‌تر خواهد شد.
براي مثال يك برنامه بزرگ را درنظر بگيريد كه به كمك قابليت Areas، به نواحي ويژه‌اي مانند گزارشگيري، قسمت ويژه مديريتي، قسمت كاربران، ناحيه بلاگ سايت، ناحيه انجمن سايت و غيره، تقسيم شده است. به علاوه هر كدام از اين نواحي نيز هنوز مي‌توانند از اطلاعات ناحيه اصلي برنامه مانند master page آن استفاده كنند. البته بايد درنظر داشت كه فايل viewStart به پوشه جاري و زير پوشه‌هاي آن اعمال مي‌شود. اگر نياز باشد تا اطلاعات اين فايل به كل برنامه اعمال شود، فقط كافي است آن‌را به يك سطح بالاتر، يعني ريشه سايت منتقل كرد.


نحوه افزودن نواحي جديد
افزودن يك Area جديد هم بسيار ساده است. بر روي نام پروژه در VS.NET كليك راست كرده و سپس گزينه Add|Area را انتخاب كنيد. سپس در صفحه باز شده، نام دلخواهي را وارد نمائيد. مثلا نام Reporting را وارد نمائيد تا ناحيه گزارشگيري برنامه از قسمت‌هاي ديگر آن مستقل شود. پس از افزودن يك Area جديد، به صورت خودكار پوشه جديدي به نام Areas به ريشه سايت اضافه مي‌شود. سپس داخل آن، پوشه‌ي ديگري به نام Reporting اضافه خواهد شد. پوشه reporting اضافه شده هم داراي پوشه‌هاي Model، Views و Controllers خاص خود مي‌باشد.
اكنون كه پوشه Areas به ريشه سايت اضافه شده است، با كليك راست بر روي اين پوشه نيز گزينه‌ي Add|Area در دسترس مي‌باشد. براي نمونه يك ناحيه جديد ديگر را به نام Admin به سايت اضافه كنيد تا بتوان امكانات مديريتي سايت را از ساير قسمت‌هاي آن مستقل كرد.


نحوه معرفي تعاريف مسيريابي نواحي تعريف شده
پس از اينكه كار با Areas را آغاز كرديم، نياز است تا با نحوه‌ي مسيريابي آن‌ها نيز آشنا شويم. براي اين منظور فايل Global.asax.cs قرار گرفته در ريشه اصلي برنامه را باز كنيد. در متد Application_Start، متدي به نام AreaRegistration.RegisterAllAreas، كار ثبت و معرفي تمام نواحي ثبت شده را به فريم ورك، به عهده دارد. كاري كه در پشت صحنه انجام خواهد شد اين است كه به كمك Reflection تمام كلاس‌هاي مشتق شده از كلاس پايه AreaRegistration به صورت خودكار يافت شده و پردازش خواهند شد. اين كلاس‌ها هم به صورت پيش فرض به نام SomeNameAreaRegistration.cs در ريشه اصلي هر Area توسط VS.NET توليد مي‌شوند. براي نمونه فايل ReportingAreaRegistration.cs توليد شده، حاوي اطلاعات زير است:

using System.Web.Mvc;

namespace MvcApplication11.Areas.Reporting
{
    public class ReportingAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "Reporting";
            }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "Reporting_default",
                "Reporting/{controller}/{action}/{id}",
                new { action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}

توسط AreaName، يك نام منحصربفرد در اختيار فريم ورك قرار خواهد گرفت. همچنين از اين نام براي ايجاد پيوند بين نواحي مختلف نيز استفاده مي‌شود.
سپس در قسمت RegisterArea، يك مسيريابي ويژه خاص ناحيه جاري مشخص گرديده است. براي مثال تمام آدرس‌هاي ناحيه گزارشگيري سايت بايد با http://localhost/reporting آغاز شوند تا مورد پردازش قرارگيرند. ساير مباحث آن هم مانند قبل است. براي مثال در اينجا نام اكشن متد پيش فرض، index تعريف شده و همچنين ذكر قسمت id نيز اختياري است.
همانطور كه ملاحظه مي‌كنيد، تعاريف مسيريابي و اطلاعات پيش فرض آن منطقي هستند و آنچنان نيازي به دستكاري و تغيير ندارند. البته اگر دقت كرده باشيد مقدار نام controller پيش فرض، مشخص نشده است. بنابراين بد نيست كه مثلا نام Home يا هر نام مورد نظر ديگري را به عنوان نام كنترلر پيش فرض در اينجا اضافه كرد.


تعاريف كنترلر‌هاي هم نام در نواحي مختلف
در ادامه مثال جاري كه دو ناحيه Admin و Reporting به آن اضافه شده، به پوشه‌هاي Controllers هر كدام، يك كنترلر جديد را به نام HomeController اضافه كنيد. همچنين اين HomeController را در ناحيه اصلي و ريشه سايت نيز اضافه نمائيد. سپس براي متد پيش فرض Index هر كدام هم يك View جديد را با كليك راست بر روي نام متد و انتخاب گزينه Add view، اضافه كنيد. اكنون برنامه را به همين نحو اجرا نمائيد. اجراي برنامه با خطاي زير متوقف خواهد شد:

Multiple types were found that match the controller named 'Home'. This can happen if the route that services this
request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request.
If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.

The request for 'Home' has found the following matching controllers:
MvcApplication11.Areas.Admin.Controllers.HomeController
MvcApplication11.Controllers.HomeController 

فوق العاده خطاي كاملي است و راه حل را هم ارائه داده است! براي اينكه مشكل ابهام يافتن HomeController برطرف شود، بايد اين جستجو را به فضاهاي نام هر قسمت از نواحي برنامه محدود كرد (چون به صورت پيش فرض فضاي نامي براي آن مشخص نشده، كل ناحيه ريشه سايت و زير مجموعه‌هاي آن‌را جستجو خواهد كرد). به همين جهت فايل 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
                , namespaces: new[] { "MvcApplication11.Controllers" }
            );
}

آرگومان چهارم معرفي شده، آرايه‌اي از نام‌هاي فضاهاي نام مورد نظر را جهت يافتن كنترلرهايي كه بايد توسط اين مسيريابي يافت شوند، تعريف مي‌كند.
اكنون اگر مجددا برنامه را اجرا كنيم، بدون مشكل View متناظر با متد Index كنترلر Home نمايش داده خواهد شد.
البته اين مشكل با نواحي ويژه و غير اصلي سايت وجود ندارد؛ چون جستجوي پيش فرض كنترلرها بر اساس ناحيه است.
در ادامه مسير http://localhost/Admin/Home را نيز در مرورگر وارد كنيد. سپس بر روي صفحه در مروگر كليك راست كرده و سورس صفحه را بررسي كنيد. مشاهده خواهيد كرد كه master page يا فايل layout ايي به آن اعمال نشده است. علت را هم در ابتداي بحث Areas مطالعه كرديد. فايل Views\_ViewStart.cshtml در سطحي كه قرار دارد به ناحيه Admin اعمال نمي‌شود. آن‌را به ريشه سايت منتقل كنيد تا layout اصلي سايت نيز به اين قسمت اعمال گردد. البته بديهي است كه هر ناحيه مي‌تواند layout خاص خودش را داشته باشد يا حتي مي‌توان با مقدار دهي خاصيت Layout نيز در هر view، فايل master page ويژه‌اي را انتخاب و معرفي كرد.


نحوه ايجاد پيوند بين نواحي مختلف سايت
زمانيكه پيوندي را به شكل زير تعريف مي‌كنيم:
@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home")

يعني ايجاد لينكي در ناحيه جاري. براي اينكه پيوند تعريف شده به ناحيه‌اي خارج از ناحيه جاري اشاره كند بايد نام Area را صريحا ذكر كرد:

@Html.ActionLink(linkText: "Home", actionName: "Index", controllerName: "Home",
                 routeValues: new { Area = "Admin" } , htmlAttributes: null)


همين نكته را بايد حين كار با متد RedirectToAction نيز درنظر داشت:
public ActionResult Index()
{
    return RedirectToAction("Index", "Home", new { Area = "Admin" });
}