۱۳۹۱/۰۱/۰۹

ASP.NET MVC #5


بررسي نحوه انتقال اطلاعات از يك كنترلر به View‌هاي مرتبط با آن

در ASP.NET Web forms در فايل code behind يك فرم مثلا مي‌توان نوشت Label1.Text و سپس مقداري را به آن انتساب داد. اما اينجا به چه ترتيبي مي‌توان شبيه به اين نوع عمليات را انجام داد؟ با توجه به اينكه در كنترلر‌ها هيچ نوع ارجاع مستقيمي به اشياء رابط كاربري وجود ندارد و اين دو از هم مجزا شده‌اند.
در پاسخ به اين سؤال، همان مثال ساده قسمت قبل را ادامه مي‌دهيم. يك پروژه جديد خالي ايجاد شده است به همراه HomeController ايي كه به آن اضافه كرده‌ايم. همچنين مطابق روشي كه ذكر شد، View ايي به نام Index را نيز به آن اضافه كرده‌ايم. سپس براي ارسال اطلاعات از يك كنترلر به View از يكي از روش‌هاي زير مي‌توان استفاده كرد:

الف) استفاده از اشياء پويا

ViewBag يك شيء dynamic است كه در دات نت 4 امكان تعريف آن ميسر شده است. به اين معنا كه هر نوع خاصيت دلخواهي را مي‌توان به اين شيء انتساب داد و سپس اين اطلاعات در View نيز قابل دسترسي و استخراج خواهد بود. مثلا اگر در اينجا به شيء ViewBag، خاصيت دلخواه Country را اضافه كنيم و سپس مقداري را نيز به آن انتساب دهيم:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Country = "Iran";
            return View();
        }
    }
}

اين اطلاعات در View مرتبط با اكشني به نام Index به نحو زير قابل بازيابي خواهد بود (نحوه اضافه كردن View متناظر با يك اكشن يا متد را هم در قسمت قبل با تصوير مرور كرديم):

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

در اين مثال، @ در View engine جاري كه Razor نام دارد، به اين معنا مي‌باشد كه اين مقداري است كه مي‌خواهم دريافت كني (ViewBag.Country) و سپس آن‌را در حين پردازش صفحه نمايش دهي.


ب) انتقال اطلاعات يك شيء كامل و غير پويا به View

هر پروژه جديد MVC به همراه پوشه‌اي به نام Models است كه در آن مي‌توان تعاريف اشياء تجاري برنامه را قرار داد. در پروژه جاري، يك كلاس ساده را به نام Employee به اين پوشه اضافه مي‌كنيم:

namespace MvcApplication1.Models
{
    public class Employee
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
    }
}

اكنون براي نمونه يك وهله از اين شيء را در متد Index ايجاد كرده و سپس به view متناظر با آن ارسال مي‌كنيم (در قسمت return View كد زير مشخص است). بديهي است اين وهله سازي در عمل مي‌تواند از طريق دسترسي به يك بانك اطلاعاتي يا يك وب سرويس و غيره باشد.

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Country = "Iran";

            var employee = new Employee
            {
                 Email = "name@site.com",
                 FirstName = "Vahid",
                 LastName = "N."
            };

            return View(employee);
        }
    }
}

امضاهاي متفاوت (overloads) متد كمكي View هم به شرح زير هستند:

ViewResult View(Object model)
ViewResult View(string viewName, Object model)
ViewResult View(string viewName, string masterName, Object model)


اكنون براي دسترسي به اطلاعات اين شيء employee در View متناظر با اين متد، چندين روش وجود دارد:

@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div>
    Country: @ViewBag.Country <‪br />
    FirstName: @Model.FirstName
</div>

مي‌توان از طريق شيء استاندارد ديگري به نام Model (كه اين هم يك شيء dynamic است مانند ViewBag قسمت قبل)، به خواص شيء يا مدل ارسالي به View جاري دسترسي پيدا كرد كه يك نمونه از آن‌را در اينجا ملاحظه مي‌كنيد.
روش دوم، بر اساس تعريف صريح نوع مدل است به نحو زير:

@model MvcApplication1.Models.Employee
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div>
    Country: @ViewBag.Country
    <‪br />
    FirstName: @Model.FirstName
</div>

در اينجا در مقايسه با قبل، تنها يك سطر به اول فايل View اضافه شده است كه در آن نوع شيء Model تعيين مي‌گردد (كلمه model هم در اينجا با حروف كوچك شروع شده است). به اين ترتيب اينبار اگر سعي كنيم به خواص اين شيء دسترسي پيدا كنيم، Intellisense ويژوال استوديو ظاهر مي‌شود. به اين معنا كه شيء Model بكارگرفته شده اينبار ديگر dynamic نيست و دقيقا مي‌داند كه چه خواصي را بايد پيش از اجراي برنامه در اختيار استفاده كننده قرار دهد.
به اين روش، روش Strongly typed view هم گفته مي‌شود؛ چون View دقيقا مي‌داند كه چون نوعي را بايد انتظار داشته باشد؛ تحت نظر كامپايلر قرار گرفته و همچنين Intellisense نيز براي آن مهيا خواهد بود.
به همين جهت اين روش Strongly typed view، در بين تمام روش‌هاي مهيا، به عنوان روش توصيه شده و مرجح مطرح است.
به علاوه استفاده از Strongly typed views يك مزيت ديگر را هم به همراه دارد: فعال شدن يك code generator توكار در VS.NET به نام scaffolding. يك مثال ساده:
تا اينجا ما اطلاعات يك كارمند را نمايش داديم. اگر بخواهيم يك ليست از كارمندها را نمايش دهيم چه بايد كرد؟
روش كار با قبل تفاوتي نمي‌كند. اينبار در return View ما، يك شيء ليستي ارائه خواهد شد. در سمت View هم با يك حلقه foreach كار نمايش اين اطلاعات صورت خواهد گرفت. راه ساده‌تري هم هست. اجازه دهيم تا خود VS.NET، كدهاي مرتبط را براي ما توليد كند.
يك كلاس ديگر به پوشه مدل‌هاي برنامه اضافه كنيد به نام Employees با محتواي زير:

using System.Collections.Generic;

namespace MvcApplication1.Models
{
    public class Employees 
    {
        public IList<Employee> CreateEmployees()
        {
            return new[]
                {
                    new Employee { Email = "name1@site.com", FirstName = "name1", LastName = "LastName1" },
                    new Employee { Email = "name2@site.com", FirstName = "name2", LastName = "LastName2" },
                    new Employee { Email = "name3@site.com", FirstName = "name3", LastName = "LastName3" }
                };
        }
    }
}

سپس متد جديد زير را به كنترلر Home اضافه كنيد.

public ActionResult List()
{
    var employeesList = new Employees().CreateEmployees();
    return View(employeesList);
}

براي اضافه كردن View متناظر با آن، روي نام متد كليك راست كرده و گزينه Add view را انتخاب كنيد. در صفحه ظاهر شده:


تيك مربوط به Create a strongly typed view را قرار دهيد. سپس در قسمت Model class، كلاس Employee را انتخاب كنيد (نه Employees جديد را، چون از آن مي‌خواهيم به عنوان منبع داده ليست توليدي استفاده كنيم). اگر اين كلاس را مشاهده نمي‌كنيد، به اين معنا است كه هنوز برنامه را يكبار كامپايل نكرده‌ايد تا VS.NET بتواند با اعمال Reflection بر روي اسمبلي برنامه آن‌را پيدا كند. سپس در قسمت Scaffold template گزينه List را انتخاب كنيد تا Code generator توكار VS.NET فعال شود. اكنون بر روي دكمه Add كليك نمائيد تا View نهايي توليد شود. براي مشاهده نتيجه نهايي مسير http://localhost/Home/List بايد بررسي گردد.


ج) استفاده از ViewDataDictionary

ViewDataDictionary از نوع IDictionary با كليدي رشته‌اي و مقداري از نوع object است. توسط آن شيء‌ايي به نام ViewData در ASP.NET MVC به نحو زير تعريف شده است:

public ViewDataDictionary ViewData { get; set; }

اين روش در نگارش‌هاي اوليه ASP.NET MVC بيشتر مرسوم بود. براي مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewData["DateTime"] = "<‪br/>" + DateTime.Now;
            return View();
        }
    }
}

و سپس جهت استفاده از اين ViewData تعريف شده با كليد دلخواهي به نام DateTime در View متناظر با اكشن Index خواهيم داشت:

@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<div>
    DateTime: @ViewData["DateTime"]
</div>

يك نكته امنيتي:
اگر به مقدار انتساب داده شده به شيء ViewDataDictionary دقت كنيد، يك تگ br هم به آن اضافه شده است. برنامه را يكبار اجرا كنيد. مشاهده خواهيد كرد كه اين تگ به همين نحو نمايش داده مي‌شود و نه به صورت يك سطر جديد HTML . چرا؟ چون Razor به صورت پيش فرض اطلاعات را encode شده (فراخواني متد Html.Encode در پشت صحنه به صورت خودكار) در صفحه نمايش مي‌دهد و اين مساله از لحاظ امنيتي بسيار عالي است؛ زيرا جلوي بسياري از حملات cross site scripting يا XSS را خواهد گرفت.
احتمالا الان اين سؤال پيش خواهد آمد كه اگر «عالمانه» بخواهيم اين رفتار نيكوي پيش فرض را غيرفعال كنيم چه بايد كرد؟
براي اين منظور مي‌توان نوشت:
@Html.Raw(myString)

و يا:
<div>@MvcHtmlString.Create("<h1>HTML</h1>")</div>

به اين ترتيب خروجي Razor ديگر encode شده نخواهد بود.


د) استفاده از TempData

TempData نيز يك dictionary ديگر براي ذخيره سازي اطلاعات است و به نحو زير در فريم ورك تعريف شده است:

public TempDataDictionary TempData { get; set; }

TempData در پشت صحنه از سشن‌هاي ASP.NET جهت ذخيره سازي اطلاعات استفاده مي‌كند. بنابراين اطلاعات آن در ساير كنترلرها و View ها نيز در دسترس خواهد بود. البته TempData يك سري تفاوت هم با سشن معمولي ASP.NET دارد:
- بلافاصله پس از خوانده شدن، حذف خواهد شد.
- پس از پايان درخواست از بين خواهد رفت.
هر دو مورد هم به جهت بالابردن كارآيي برنامه‌هاي ASP.NET MVC و مصرف كمتر حافظه سرور درنظر گرفته‌ شده‌اند.
البته كساني كه براي بار اول هست با ASP.NET مواجه مي‌شوند، شايد سؤال بپرسند اين مسايل چه اهميتي دارد؟ پروتكل HTTP، ذاتا يك پروتكل «بدون حالت» است يا Stateless هم به آن گفته مي‌شود. به اين معنا كه پس از ارائه يك صفحه وب توسط سرور، تمام اشياء مرتبط با آن در سمت سرور تخريب خواهند شد. اين مورد متفاوت‌ است با برنامه‌هاي معمولي دسكتاپ كه طول عمر يك شيء معمولي تعريف شده در سطح فرم به صورت يك فيلد، تا زمان باز بودن آن فرم، تعيين مي‌گردد و به صورت خودكار از حافظه حذف نمي‌شود. اين مساله دقيقا مشكل تمام تازه واردها به دنياي وب است كه چرا اشياء ما نيست و نابود شدند. در اينجا وب سرور قرار است به هزاران درخواست رسيده پاسخ دهد. اگر قرار باشد تمام اين اشياء را در سمت سرور نگهداري كند، خيلي زود با اتمام منابع مواجه مي‌گردد. اما واقعيت اين است كه نياز است يك سري از اطلاعات را در حافظه نگه داشت. به همين منظور يكي از چندين روش مديريت حالت در ASP.NET استفاده از سشن‌ها است كه در اينجا به نحو بسيار مطلوبي، با سربار حداقل توسط TempData مديريت شده است.
يك مثال كاربردي در اين زمينه:
فرض كنيد در متد جاري كنترلر، ابتدا بررسي مي‌كنيم كه آيا ورودي دريافتي معتبر است يا خير. در غيراينصورت، كاربر را به يك View ديگر از طريق كنترلري ديگر جهت نمايش خطاها هدايت خواهيم كرد.
همين «هدايت مرورگر به يك View ديگر» يعني پاك شدن و تخريب اطلاعات كنترلر قبلي به صورت خودكار. بنابراين نياز است اين اطلاعات را در TempData قرار دهيم تا در كنترلري ديگر قابل استفاده باشد:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult InsertData(string name)
        {
            // Check for input errors.
            if (string.IsNullOrWhiteSpace(name))
            {
                TempData["error"] = "name is required.";
                return RedirectToAction("ShowError");
            }
            // No errors
            // ...
            return View();
        }

        public ActionResult ShowError()
        {
            var error = TempData["error"] as string;
            if (!string.IsNullOrWhiteSpace(error))
            {
                ViewBag.Error = error;
            }
            return View();
        }
    }
}

در همان HomeController دو متد جديد به نام‌هاي InsertData و ShowError اضافه شده‌اند. در متد InsertData ابتدا بررسي مي‌شود كه آيا نامي وارد شده است يا خير. اگر خير توسط متد RedirectToAction، كاربر به اكشن يا متد ShowError هدايت خواهد شد.
براي انتقال اطلاعات خطايي كه مي‌خواهيم در حين اين Redirect نمايش دهيم نيز از TempData استفاده شده است.
بديهي است براي اجرا اين مثال نياز است دو View جديد براي متدهاي InsertData و ShowError ايجاد شوند (كليك راست روي نام متد و انتخاب گزينه Add view براي اضافه كردن View مرتبط با آن اكشن).
محتواي View مرتبط با متد افزودن اطلاعات فعلا مهم نيست، ولي View نمايش خطاها در ساده‌ترين حالت مثلا مي‌تواند به صورت زير باشد:

@{
    ViewBag.Title = "ShowError";
}

<h2>Error</h2>

@ViewBag.Error

براي آزمايش برنامه هم مطابق مسيريابي پيش فرض و با توجه به قرار داشتن در كنترلري به نام Home، مسير http://localhost/Home/InsertData ابتدا بايد بررسي شود. چون آرگوماني وارد نشده، بلافاصله صفحه به آدرس http://localhost/Home/ShowError به صورت خودكار هدايت خواهد شد.


نكته‌اي تكميلي در مورد Strongly typed viewها:
عنوان شد كه Strongly typed view روش مرجح بوده و بهتر است از آن استفاده شود، زيرا اطلاعات اشياء و خواص تعريف شده در يك View تحت نظر كامپايلر قرار مي‌گيرند كه بسيار عالي است. يعني اگر در View بنويسم FirstName: @Model.FirstName1 چون FirstName1 وجود خارجي ندارد، برنامه نبايد كامپايل شود. يكبار اين را بررسي كنيد. برنامه بدون مشكل كامپايل مي‌شود! اما تنها در زمان اجرا است كه صفحه زرد رنگ معروف خطاهاي ASP.NET ظاهر مي‌شود كه چنين خاصيتي وجود ندارد (اين حالت پيش فرض است؛ يعني كامپايل يك View‌ در زمان اجرا). البته اين باز هم خيلي بهتر است از ViewBag، چون اگر مثلا ViewBag.Country1 را وارد كنيم، در زمان اجرا تنها چيزي نمايش داده نخواهد شد؛‌ اما با روش Strongly typed view، حتما خطاي Compilation Error به همراه نمايش محل مشكل نهايي، در صفحه ظاهر خواهد شد.
سؤال: آيا مي‌شود پيش از اجراي برنامه هم اين بررسي را انجام داد؟
پاسخ: بله. بايد فايل پروژه را اندكي ويرايش كرده و مقدار MvcBuildViews را كه به صورت پيش فرض false هست، true نمود. يا خارج از ويژوال استوديو با يك اديتور متني ساده مثلا فايل csproj را گشوده و اين تغيير را انجام دهيد. يا داخل ويژوال استوديو، بر روي نام پروژه كليك راست كرده و سپس گزينه Unload Project را انتخاب كنيد. مجددا بر روي اين پروژه Unload شده كليك راست نموده و گزينه edit را انتخاب نمائيد. در صفحه باز شده، MvcBuildViews را يافته و آن‌را true كنيد. سپس پروژه را Reload كنيد.
اكنون اگر پروژه را كامپايل كنيد، پيغام خطاي زير پيش از اجراي برنامه قابل مشاهده خواهد بود:

'MvcApplication1.Models.Employee' does not contain a definition for 'FirstName1' 
and no extension method 'FirstName1' accepting a first argument of type 'MvcApplication1.Models.Employee'
could be found (are you missing a using directive or an assembly reference?)
d:\Prog\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml 10 MvcApplication1

البته بديهي است اين تغيير، زمان Build پروژه را مقداري افزايش خواهد داد؛ اما امن‌ترين حالت ممكن براي جلوگيري از اين نوع خطاهاي تايپي است.
يا حداقل بهتر است يكبار پيش از ارائه نهايي برنامه اين مورد فعال و بررسي شود.

و يك خبر خوب!
مجوز سورس كد ASP.NET MVC از MS-PL به Apache تغيير كرده و همچنين Razor و يك سري موارد ديگر هم سورس باز شده‌اند. اين تغييرات به اين معنا خواهند بود كه پروژه از حالت فقط خواندني MS-PL به حالت متداول يك پروژه سورس باز كه شامل دريافت تغييرات و وصله‌ها از جامعه برنامه نويس‌ها است، تغيير كرده است (^ و ^).