۱۳۹۱/۰۱/۱۵

ASP.NET MVC #9


مروري بر HTML Helpers استاندارد مهيا در ASP.NET MVC

يكي از اهداف وجودي Server controls در ASP.NET Web forms، رندر خودكار HTML است. براي مثال Menu control، TreeView control، GridView و امثال آن كار توليد تگ‌هاي table، tr و بسياري موارد ديگر را در پشت صحنه براي ما انجام مي‌دهند. اما در ASP.NET MVC، هدف رسيدن به يك markup ساده و تميز است كه 100 درصد بر روي اجزاي آن كنترل داشته باشيم و اين مورد به صورت ضمني به اين معنا است كه در اينجا تمام اين HTMLها را بايد خودمان توليد كنيم. البته در عمل خير. يك نمونه از آن‌را در قسمت قبل مشاهده كرديم كه چطور مي‌توان منطق توليد تگ‌هاي HTML را كپسوله سازي كرد و بارها مورد استفاده قرار داد. به علاوه فريم ورك ASP.NET MVC نيز به همراه تعدادي HTML helper توكار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غيره كه كار توليد تگ‌هاي HTML ضروري و پايه را براي ما ساده مي‌كنند.
يك مثال:
@Html.ActionLink("About us", "Index", "About")

در اينجا از متدي به نام ActionLink استفاده شده است. شيء Html هم وهله‌اي از كلاس HtmlHelper است كه در تمام Viewها قابل دسترسي مي‌باشد.
در اين متد،‌ اولين پارامتر، متن نمايش داده شده به كاربر را مشخص مي‌كند، پارامتر سوم، نام كنترلري است كه مورد استفاده قرار مي‌گيرد و پارامتر دوم، نام متد يا اكشني در آن است كه فراخواني خواهد شد (البته هر كدام از اين HtmlHelperها به همراه تعداد قابل توجهي overload هم هستند).
زمانيكه اين صفحه را رندر كنيم، به خروجي زير خواهيم رسيد:
<a href="/About">About us</a>

در اين لينك نهايي خبري از متد Index ايي كه معرفي كرديم، نيست. چرا؟
متد ActionLink بر اساس تعاريف پيش فرض مسيريابي برنامه، سعي مي‌كند بهترين خروجي را ارائه دهد. مطابق تعاريف پيش فرض برنامه، متد Index، اكشن پيش فرض كنترلرهاي برنامه است. بنابراين ضرورتي به ذكر آن نديده است.

مثالي ديگر:
همان كلاس‌هاي Product و Products قسمت هفتم را در نظر بگيريد (قسمت بررسي «ساختار پروژه مثال جاري» در آن مثال). همچنين به اطلاعات «نوشتن HTML Helpers ويژه، به كمك امكانات Razor» قسمت هشتم هم نياز داريم.
اينبار مي‌خواهيم بجاي نمايش ليست ساده‌اي از محصولات،‌ ابتدا نام آن‌ها را به صورت لينك‌هايي در صفحه نمايش دهيم. در ادامه پس از كليك كاربر روي يك نام، توضيحات بيشتري از محصول انتخابي را در صفحه‌اي ديگر ارائه نمائيم. كدهاي View ما اينبار به شكل زير تغيير مي‌كنند:

@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
    ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
    {
    <ul>
        @foreach (var item in products)
        {
            <li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
        }
    </ul>
}
<h2>Index</h2>

@GetProductsList(@Model)

توضيحات:
ابتدا يك helper method را تعريف كرده‌ايم و به كمك Html.ActionLink، از نام و شماره محصول، جهت توليد لينك‌هاي نمايش جزئيات هر يك از محصولات كمك گرفته‌ايم. بنابراين در كنترلر خود نياز به متد جديدي به نام Details خواهيم داشت كه پارامتري از نوع ProductNumber را دريافت مي‌كند. سپس جزئيات اين محصول را يافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومي كه در متد ActionLink بكارگرفته شده در اينجا مشاهده مي‌كنيد، يك anonymously typed object است و توسط آن خواصي را تعريف خواهيم كرد كه توسط تعاريف مسيريابي تعريف شده در فايل Global.asax.cs،‌ قابل تفسير و تبديل به لينك‌هاي مرتبط و صحيحي باشد.
اكنون اگر اين مثال را اجرا كنيم، اولين لينك توليدي آن به اين شكل خواهد بود:
http://localhost/Home/Details/D123

در اينجا به يك نكته مهم هم بايد دقت داشت؛ نام كنترلر به صورت خودكار به اين لينك اضافه شده است. بنابراين بهتر است از ايجاد دستي اين نوع لينك‌ها خودداري كرده و كار را به متدهاي استاندارد فريم ورك واگذار نمود تا بهترين خروجي را دريافت كنيم.
البته اگر الان بر روي اين لينك كليك نمائيم، با پيغام 404 مواجه خواهيم شد. براي تكميل اين مثال، متد Details را به كنترلر تعريف شده اضافه خواهيم كرد:

using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;

namespace MvcApplication5.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var products = new Products();
            return View(products);
        }

        public ActionResult Details(string id)
        {
            var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
            if (product == null)
                return View("Error");
            return View(product);
        }
    }
}

در متد Details، ابتدا ProductNumber دريافت شده و سپس شيء محصول متناظر با آن، به View اين متد، بازگشت داده مي‌شود. اگر بر اساس ورودي دريافتي، محصولي يافت نشد، كاربر را به View ايي به نام Error كه در پوشه Views/Shared قرار گرفته است، هدايت مي‌كنيم.
براي اضافه كردن اين View هم بر روي متد كليك راست كرده و گزينه Add view را انتخاب كنيد. چون يك شيء strongly typed از نوع Product را قرار است به View ارسال كنيم (مانند مثال قسمت پنجم)، مي‌توان در صفحه باز شده تيك Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب كرد و در قسمت Scaffold template هم Details را انتخاب نمود. به اين ترتيب Code generator توكار VS.NET قسمتي از كار توليد View را براي ما انجام داده و بديهي است اكنون سفارشي سازي اين View توليدي كه قسمت عمده‌اي از آن توليد شده است، كار ساده‌اي مي‌باشد:

@model MvcApplication5.Models.Product

@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
    <legend>Product</legend>

    <div class="display-label">ProductNumber</div>
    <div class="display-field">@Model.ProductNumber</div>

    <div class="display-label">Name</div>
    <div class="display-field">@Model.Name</div>

    <div class="display-label">Price</div>
    <div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
    @Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
    @Html.ActionLink("Back to List", "Index")
</p>

در اينجا كدهاي مرتبط با View نمايش جزئيات محصول را مشاهده مي‌كنيد كه توسط VS.NET به صورت خودكار از روي مدل انتخابي توليد شده است.
اكنون يكبار ديگر برنامه را اجرا كرده و بر روي لينك نمايش جزئيات محصولات كليك نمائيد تا بتوان اين اطلاعات را در صفحه‌ي بعدي مشاهده نمود.


يك نكته:
اگر سعي كنيم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهيم، به متد Html.ActionLink دسترسي نخواهيم داشت. چرا؟
پيغام خطايي كه ارائه مي‌شود اين است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink' 

به اين معنا كه در وهله‌اي از شيء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink مي‌گردد. در حاليكه ActionLink مورد نظر به كلاس System.Web.Mvc.HtmlHelper مرتبط مي‌شود.
يك راه حل آن به صورت زير است. به هر متد helper يك آرگومان WebViewPage page را اضافه مي‌كنيم (به همراه دو فضاي نامي كه به ابتداي فايل اضافه مي‌شوند)

@using System.Web.Mvc
@using System.Web.Mvc.Html

@using MvcApplication5.Models

@helper GetProductsList(WebViewPage page, List<Product> products)
    {
    <ul>
        @foreach (var item in products)
        {
            <li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
        }
    </ul>
}
سپس براي استفاده از آن در يك View خواهيم داشت:
@MyHelpers.GetProductsList(this, @Model)


متد ActionLink و عبارات فارسي

متد ActionLink آدرس‌هاي وبي را كه توليد مي‌كند، URL encoded هستند. براي نمونه اگر رشته‌اي كه قرار است به عنوان پارامتر به اكشن متد ما ارسال شود، مساوي Hello World است، آن‌را به صورت Hello%20World در صفحه درج مي‌كند. البته اين مورد مشكلي را در سمت متدهاي كنترلرها ايجاد نمي‌كند، چون كار URL decoding خودكار است. اما ... اگر مقداري كه قرار است ارسال شود مثلا «مقدار يك» باشد، آدرس توليدي اين شكل را خواهد داشت:

http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83

و اگر اين URL encoding انجام نشود، فقط اولين قسمت قبل از فاصله به متد ارسال مي‌گردد.
مرورگرهايي مثل فايرفاكس و كروم، مشكلي با نمايش اين لينك به شكل اصلي فارسي آن ندارند (حين نمايش، URL decoding را اعمال مي‌كنند). اما اگر مرورگر مثلا IE8 باشد، كاربر دقيقا به همين شكل آدرس‌ها را در نوار آدرس مرورگر خود مشاهده خواهد كرد كه آنچنان زيبا نيستند.
حل اين مشكل، يك نكته كوچك را به همراه دارد. اگر href توليدي به شكل زير باشد:

<li><a href="/Home/Details/مقدار يك">Super Fast Bike</a></li>

IE حين نمايش نهايي آن، آن‌را فارسي نشان خواهد داد. حتي زمانيكه كاربر بر روي آن كليك كند، به صورت خودكار كاراكترهايي را كه لازم است encode نمايد، به نحو صحيحي در URL نهايي قابل مشاهده در نوار آدرس‌ها ظاهر خواهد كرد. براي مثال %20 را به صورت خودكار اضافه مي‌كند و نگراني از اين لحاظ وجود نخواهد داشت كه الان بين دو كلمه فاصله‌اي وجود دارد يا خير (مرورگرهاي ديگر هم دقيقا همين رفتار را در مورد لينك‌هاي داخل صفحه دارند).
خلاصه اين توضيحات متد كمكي زير است:

@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
    @Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}

و براي نمونه نحوه استفاده از آن به شكل زير خواهد بود:

@helper GetProductsList(List<Product> products)
    {
    <ul>
        @foreach (var item in products)
        {
            <li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
        }
    </ul>
}

ضمن اينكه بايد درنظر داشت كلا اين نوع طراحي مشكل دارد! براي مثال فرض كنيد كه در اين مثال، جزئيات، نمايش دهنده مطلب ارسالي در يك بلاگ است. يعني يك سري عنوان و جزئيات متناظر با آن‌ها در ديتابيس وجود دارند. اگر آدرس مطالب به اين شكل باشد http://site/blog/details/text، به اين معنا است كه اين text مساوي است با primary key جدول بانك اطلاعاتي. يعني وبلاگ نويس سايت شما فقط يكبار در طول عمر اين برنامه مي‌تواند بگويد «سال نو مبارك!». دفعه‌ي بعد به علت تكراري بودن، مجاز به ارسال پيام تبريك ديگري نخواهد بود! به همين جهت بهتر است طراحي را به اين شكل تغيير دهيد http://site/blog/details/id/text. در اينجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشايند بانك اطلاعاتي و Text هم براي خوشايند موتورهاي جستجو در اين URL قرار دارند. مطابق تعاريف مسيريابي برنامه، Text فقط حالت تزئيني داشته و پردازش نخواهد شد.
از اين نوع ترفندها زياد به كار برده مي‌شوند. براي نمونه به URL مطالب انجمن‌هاي معروف اينترنتي دقت كنيد. عموما يك عدد را به همراه text مشاهده مي‌كنيد. عدد در برنامه پردازش مي‌شود، متن هم براي موتورهاي جستجو درنظر گرفته شده است.