۱۳۹۱/۰۱/۱۹

ASP.NET MVC #11


بررسي نكات تكميلي Model binder در ASP.NET MVC

يك برنامه خالي جديد ASP.NET MVC را شروع كنيد و سپس مدل زير را به پوشه Models آن اضافه نمائيد:

using System;

namespace MvcApplication7.Models
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string Password { set; get; }
        public DateTime AddDate { set; get; }
        public bool IsAdmin { set; get; }
    }
}

از اين مدل چند مقصود ذيل دنبال مي‌شوند:
استفاده از Id به عنوان primary key براي edit و update ركوردها. استفاده از DateTime براي اينكه اگر كاربري اطلاعات بي ربطي را وارد كرد چگونه بايد اين مشكل را در حالت model binding خودكار تشخيص داد و استفاده از IsAdmin براي يادآوري يك نكته امنيتي بسيار مهم كه اگر حين model binding خودكار به آن توجه نشود، سايت را با مشكلات حاد امنيتي مواجه خواهد كرد. سيستم پيشرفته است. مي‌تواند به صورت خودكار ورودي‌هاي كاربر را تبديل به يك شيء حاضر و آماده كند ... اما بايد حين استفاده از اين قابليت دلپذير به يك سري نكات امنيتي هم دقت داشت تا سايت ما به نحو دلپذيري هك نشود!

در ادامه يك كنترلر جديد به نام UserController را به پوشه كنترلرهاي پروژه اضافه نمائيد. همچنين نام كنترلر پيش فرض تعريف شده در قسمت مسيريابي فايل Global.asax.cs را هم به User تغيير دهيد تا در هربار اجراي برنامه در VS.NET، نيازي به تايپ آدرس‌هاي مرتبط با UserController نداشته باشيم.
يك منبع داده تشكيل شده در حافظه را هم براي نمايش ليستي از كاربران، به نحو زير به پروژه اضافه خواهيم كرد:

using System;
using System.Collections.Generic;

namespace MvcApplication7.Models
{
    public class Users
    {
        public IList<User> CreateInMemoryDataSource()
        {
            return new[]
            {
                new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
                new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
                new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
            };
        }
    }
}

در اينجا فعلا هدف آشنايي با زير ساخت‌هاي ASP.NET MVC است و درك صحيح نحوه كاركرد آن. مهم نيست از EF استفاده مي‌كنيد يا NH يا حتي ADO.NET كلاسيك و يا از Micro ORMهايي كه پس از ارائه دات نت 4 مرسوم شده‌اند. تهيه يك ToList يا Insert و Update با اين فريم ورك‌ها خارج از بحث جاري هستند.

سورس كامل كنترلر User به شرح زير است:

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

namespace MvcApplication7.Controllers
{
    public class UserController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var usersList = new Users().CreateInMemoryDataSource();
            return View(usersList); // Shows the Index view.
        }

        [HttpGet]
        public ActionResult Details(int id)
        {
            var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
            if (user == null)
                return View("Error");
            return View(user); // Shows the Details view.
        }

        [HttpGet]
        public ActionResult Create()
        {
            var user = new User { AddDate = DateTime.Now };
            return View(user); // Shows the Create view.
        }

        [HttpPost]
        public ActionResult Create(User user)
        {
            if (this.ModelState.IsValid)
            {
                // todo: Add record
                return RedirectToAction("Index");
            }
            return View(user); // Shows the Create view again.
        }

        [HttpGet]
        public ActionResult Edit(int id)
        {
            var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
            if (user == null)
                return View("Error");
            return View(user); // Shows the Edit view.
        }

        [HttpPost]
        public ActionResult Edit(User user)
        {
            if (this.ModelState.IsValid)
            {
                // todo: Edit record
                return RedirectToAction("Index");
            }
            return View(user); // Shows the Edit view again.
        }

        [HttpPost]
        public ActionResult Delete(int id)
        {
            // todo: Delete record
            return RedirectToAction("Index");
        }
    }
}

توضيحات:

ايجاد خودكار فرم‌هاي ورود اطلاعات

در قسمت قبل براي توضيح دادن نحوه ايجاد فرم‌ها در ASP.NET MVC و همچنين نحوه نگاشت اطلاعات آن‌ها به اكشن متدهاي كنترلرها، فرم‌هاي مورد نظر را دستي ايجاد كرديم.
اما بايد درنظر داشت كه براي ايجاد Viewها مي‌توان از ابزار توكار خود VS.NET نيز استفاده كرد و سپس اطلاعات و فرم‌هاي توليدي را سفارشي نمود. اين سريع‌ترين راه ممكن است زمانيكه مدل مورد استفاده كاملا مشخص است و مي‌خواهيم Strongly typed views را ايجاد كنيم.
براي نمونه بر روي متد Index كليك راست كرده و گزينه Add view را انتخاب كنيد. در اينجا گزينه‌ي create a strongly typed view را انتخاب كرده و سپس از ليست مدل‌ها، User را انتخاب نمائيد. Scaffold template را هم بر روي حالت List قرار دهيد.
براي متد Details هم به همين نحو عمل نمائيد.
براي ايجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل يكي است. فقط Scaffold template انتخابي را بر روي Create قرار دهيد تا فرم ورود اطلاعات، به صورت خودكار توليد شود.
متد Create در حالت HttpPost نيازي به View اضافي ندارد. چون صرفا قرار است اطلاعاتي را از سرور دريافت و ثبت كند.
براي ايجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با اين تفاوت كه Scaffold template انتخابي را بر روي گزينه Edit قرار دهيد تا فرم ويرايش اطلاعات كاربر به صورت خودكار به پروژه اضافه شود.
متد Edit در حالت HttpPost نيازي به View اضافي ندارد و كارش تنها دريافت اطلاعات از سرور و به روز رساني بانك اطلاعاتي است.
به همين ترتيب متد Delete نيز، نيازي به View خاصي ندارد. در اينجا بر اساس primary key دريافتي، مي‌توان يك كاربر را يافته و حذف كرد.



سفارشي سازي Viewهاي خودكار توليدي

با كمك امكانات Scaffolding نامبرده شده، حجم قابل توجهي كد را در اندك زماني مي‌توان توليد كرد. بديهي است حتما نياز به سفارشي سازي كدهاي توليدي وجود خواهد داشت. مثلا شايد نيازي نباشد فيلد پسود كاربر، در حين نمايش ليست كاربران، نمايش داده شود. مي‌شود كلا اين ستون را حذف كرد و از اين نوع مسايل.
يك مورد ديگر را هم در Viewهاي توليدي حتما نياز است كه ويرايش كنيم. آن هم مرتبط است به لينك حذف اطلاعات يك كاربر در صفحه Index.cshtml:

@Html.ActionLink("Delete", "Delete", new { id=item.Id }

در قسمت قبل هم عنوان شد كه اعمال حذف بايد بر اساس HttpPost محدود شوند تا بتوان ميزان امنيت برنامه را بهبود داد. متد Delete هم در كنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراين ActionLink پيش فرض را حذف كرده و بجاي آن فرم و دكمه زير را قرار مي‌دهيم تا اطلاعات به سرور Post شوند:

@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
    <input type="submit" value="Delete" 
              onclick="return confirm ('Do you want to delete this record?');" />
}

در اينجا نحوه ايجاد يك فرم، كه id ركورد متناظر را به سرور ارسال مي‌كند، مشاهده مي‌كنيد.



علت وجود دو متد، به ازاي هر Edit يا Create

به ازاي هر كدام از متدهاي Edit و Create دو متد HttpGet و HttpPost را ايجاد كرده‌ايم. كار متدهاي HttpGet نمايش View‌هاي متناظر به كاربر هستند. بنابراين وجود آن‌ها ضروري است. در اين حالت چون از دو Verb متفاوت استفاده شده، مي‌توان متدهاي هم نامي را بدون مشكل استفاده كرد. به هر كدام از افعال Get و Post و امثال آن، يك Http Verb گفته مي‌شود.



بررسي معتبر بودن اطلاعات دريافتي

كلاس پايه Controller كه كنترلرهاي برنامه از آن مشتق مي‌شوند، شامل يك سري خواص و متدهاي توكار نيز هست. براي مثال توسط خاصيت this.ModelState.IsValid مي‌توان بررسي كرد كه آيا Model دريافتي معتبر است يا خير. براي بررسي اين مورد، يك breakpoint را بر روي سطر this.ModelState.IsValid در متد Create قرار دهيد. سپس به صفحه ايجاد كاربر جديد مراجعه كرده و مثلا بجاي تاريخ روز، abcd را وارد كنيد. سپس فرم را به سرور ارسال نمائيد. در اين حالت مقدار خاصيت this.ModelState.IsValid مساوي false مي‌باشد كه حتما بايد به آن پيش از ثبت اطلاعات دقت داشت.



شبيه سازي عملكرد ViewState در ASP.NET MVC

در متدهاي Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شيء User دريافتي، به View بازگشت داده مي‌شود. چرا؟
صفحات وب، زمانيكه به سرور ارسال مي‌شوند، تمام اطلاعات كنترل‌هاي خود را از دست خواهد داد (صفحه پاك مي‌شود، چون مجددا يك صفحه خالي از سرور دريافت خواهد شد). براي رفع اين مشكل در ASP.NET Web forms، از مفهومي به نام ViewState كمك مي‌گيرند. كار ViewState ذخيره موقت اطلاعات فرم جاري است براي استفاده مجدد پس از Postback. به اين معنا كه پس از ارسال فرم به سرور، اگر كاربري در textbox اول مقدار abc را وارد كرده بود، پس از نمايش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد كرد (شبيه سازي برنامه‌هاي دسكتاپ در محيط وب). بديهي است وجود ViewState براي ذخيره سازي اين نوع اطلاعات، حجم صفحه را بالا مي‌برد (بسته به پيچيدگي صفحه ممكن است به چند صد كيلوبايت هم برسد).
در ASP.NET MVC بجاي استفاده از ترفندي به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت مي‌دهند. در اين حالت پس از ارسال صفحه به سرور و نمايش مجدد صفحه ورود اطلاعات، تمام كنترل‌ها با همان مقادير قبلي وارد شده توسط كاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed مي‌باشد. در اين حالت فريم ورك مي‌داند كه اطلاعات را چگونه به كنترل‌هاي قرار گرفته در صفحه نگاشت كند).
در مثال فوق، اگر اطلاعات وارد شده صحيح باشند، كاربر به صفحه Index هدايت خواهد شد. در غيراينصورت مجددا همان View جاري با همان اطلاعات model قبلي كه كاربر تكميل كرده است به او براي تصحيح، نمايش داده مي‌شود. اين مساله هم جهت بالا بردن سهولت كاربري برنامه بسيار مهم است. تصور كنيد كه يك فرم خالي با پيغام «تاريخ وارد شده معتبر نيست» مجدا به كاربر نمايش داده شود و از او درخواست كنيم كه تمام اطلاعات ديگر را نيز از صفر وارد كند چون اطلاعات صفحه پس از ارسال به سرور پاك شده‌اند؛ كه ... اصلا قابل قبول نيست و فوق‌العاده برنامه را غيرحرفه‌اي نمايش مي‌دهد.



خطاهاي نمايش داده شده به كاربر

به صورت پيش فرض خطايي كه به كاربر نمايش داده مي‌شود، استثنايي است كه توسط فريم ورك صادر شده است. براي مثال نتوانسته است abcd را به يك تاريخ معتبر تبديل كند. مي‌توان توسط this.ModelState.AddModelError خطايي را نيز در اينجا اضافه كرد و پيغام بهتري را به كاربر نمايش داد. يا توسط يك سري data annotations هم كار اعتبار سنجي را سفارشي كرد كه بحث آن به صورت جداگانه در يك قسمت مستقل بررسي خواهد شد.
ولي به صورت خلاصه اگر به فرم‌هاي توليد شده توسط VS.NET دقت كنيد، در ابتداي هر فرم داريم:

@Html.ValidationSummary(true)

در اينجا خطاهاي عمومي در سطح مدل نمايش داده مي‌شوند. براي اضافه كردن اين نوع خطاها، در متد AddModelError، مقدار key را خالي وارد كنيد:

ModelState.AddModelError(string.Empty, "There is something wrong with model.");

همچنين در اين فرم‌ها داريم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)

EditorFor سعي مي‌كند اندكي هوش به خرج دهد. يعني اگر خاصيت دريافتي مثلا از نوع bool بود، خودش يك checkbox را در صفحه نمايش مي‌دهد. همچنين بر اساس متاديتا يك خاصيت نيز مي‌تواند تصميم گيري را انجام دهد. اين متاديتا منظور attributes و data annotations ايي است كه به خواص يك مدل اعمال مي‌شود. مثلا اگر ويژگي HiddenInput را به يك خاصيت اعمال كنيم، به شكل يك فيلد مخفي در صفحه ظاهر خواهد شد.
يا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندني نمايش مي‌دهد. اصطلاحا به اين نوع متدها، Templated Helpers هم گفته مي‌شود. بحث بيشتر درباره‌اي اين موارد به قسمتي مجزا و مستقل موكول مي‌گردد. براي نمونه كل فرم اديت برنامه را حذف كنيد و بجاي آن بنويسيد Html.EditorForModel و سپس برنامه را اجرا كنيد. يك فرم كامل خودكار ويرايش اطلاعات را مشاهده خواهيد كرد (و البته نكات سفارشي سازي آن به يك قسمت كامل نياز دارند).
در اينجا متد ValidationMessageFor كار نمايش خطاهاي اعتبارسنجي مرتبط با يك خاصيت مشخص را انجام مي‌دهد. بنابراين اگر قصد ارائه خطايي سفارشي و مخصوص يك فيلد مشخص را داشتيد، در متد AddModelError، مقدار پارامتر اول يا همان key را مساوي نام خاصيت مورد نظر قرار دهيد.


مقابله با مشكل امنيتي Mass Assignment در حين كار با Model binders

استفاده از Model binders بسيار لذت بخش است. يك شيء را به عنوان پارامتر اكشن متد خود معرفي مي‌كنيم. فريم ورك هم در ادامه سعي مي‌كند تا اطلاعات فرم را به خواص اين شيء نگاشت كند. بديهي است اين روش نسبت به روش ASP.NET Web forms كه بايد به ازاي تك تك كنترل‌هاي موجود در صفحه يكبار كار دريافت اطلاعات و مقدار دهي خواص يك شيء را انجام داد، بسيار ساده‌تر و سريعتر است.
اما اگر همين سيستم پيشرفته جديد ناآگاهانه مورد استفاده قرار گيرد مي‌تواند منشاء حملات ناگواري شود كه به نام «Mass Assignment» شهرت يافته‌اند.
همان صفحه ويرايش اطلاعات را درنظر بگيريد. چك باكس IsAdmin قرار است در قسمت مديريتي برنامه تنظيم شود. اگر كاربري نياز داشته باشد اطلاعات خودش را ويرايش كند، مثلا پسوردش را تغيير دهد، با يك صفحه ساده كلمه عبور قبلي را وارد كنيد و دوبار كلمه عبور جديد را نيز وارد نمائيد، مواجه خواهد شد. خوب ... اگر همين كاربر صفحه را جعل كند و فيلد چك باكس IsAdmin را به صفحه اضافه كند چه اتفاقي خواهد افتاد؟ بله ... مشكل هم همينجا است. در اينصورت كاربر عادي مي‌تواند دسترسي خودش را تا سطح ادمين بالا ببرد، چون model binder اطلاعات IsAdmin را از كاربر دريافت كرده و به صورت خودكار به model ارائه شده، نگاشت كرده است.
براي مقابله با اين نوع حملات چندين روش وجود دارند:
الف) ايجاد ليست سفيد
به كمك ويژگي Bind مي‌توان ليستي از خواص را جهت به روز رساني به model binder معرفي كرد. مابقي نديد گرفته خواهند شد:

public ActionResult Edit([Bind(Include = "Name, Password")] User user)

در اينجا تنها خواص Name و Password توسط model binder به خواص شيء User نگاشت مي‌شوند.
به علاوه همانطور كه در قسمت قبل نيز ذكر شد، متد edit را به شكل زير نيز مي‌توان بازنويسي كرد. در اينجا متدهاي توكار UpdateModel و TryUpdateModel نيز ليست سفيد خواص مورد نظر را مي‌پذيرند (اعمال دستي model binding):

[HttpPost]
public ActionResult Edit()
{
    var user = new User();
    if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
    {                
                // todo: Edit record
                return RedirectToAction("Index");
     }
     return View(user); // Shows the Edit view again.
}


ب) ايجاد ليست سياه
به همين ترتيب مي‌توان تنها خواصي را معرفي كرد كه بايد صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)

در اينجا از خاصيت IsAdmin صرف نظر گرديده و از مقدار ارسالي آن توسط كاربر استفاده نخواهد شد.
و يا مي‌توان پارامتر excludeProperties متد TryUpdateModel را نيز مقدار دهي كرد.

لازم به ذكر است كه ويژگي Bind را به كل يك كلاس هم مي‌توان اعمال كرد. براي مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication7.Models
{
    [Bind(Exclude = "IsAdmin")]
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string Password { set; get; }
        public DateTime AddDate { set; get; }
        public bool IsAdmin { set; get; }
    }
}

اين مورد اثر سراسري داشته و قابل بازنويسي نيست. به عبارتي حتي اگر در متدي خاصيت IsAdmin را مجددا الحاق كنيم، تاثيري نخواهد داشت.
يا مي‌توان از ويژگي ReadOnly هم استفاده كرد:
using System;
using System.ComponentModel;

namespace MvcApplication7.Models
{
    public class User
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public string Password { set; get; }
        public DateTime AddDate { set; get; }

        [ReadOnly(true)]
        public bool IsAdmin { set; get; }
    }
}

در اين حالت هم خاصيت IsAdmin هيچگاه توسط model binder به روز و مقدار دهي نخواهد شد.

ج) استفاده از ViewModels
اين راه حلي است كه بيشتر مورد توجه معماران نرم افزار است و البته كساني كه پيشتر با الگوي MVVM كار كرده باشند اين نام برايشان آشنا است؛ اما در اينجا مفهوم متفاوتي دارد. در الگوي MVVM، كلاس‌هاي ViewModel شبيه به كنترلرها در MVC هستند يا به عبارتي همانند رهبر يك اكستر عمل مي‌كنند. اما در الگوي MVC خير. در اينجا فقط مدل يك View هستند و نه بيشتر. هدف هم اين است كه بين Domain Model و View Model تفاوت قائل شد.
كار View model در الگوي MVC، شكل دادن به چندين domain model و همچنين اطلاعات اضافي ديگري كه نياز هستند، جهت استفاده نهايي توسط يك View مي‌باشد. به اين ترتيب View با يك شيء سر و كار خواهد داشت و همچنين منطق شكل دهي به اطلاعات مورد نيازش هم از داخل View حذف شده و به خواص View model در زمان تشكيل آن منتقل مي‌شود.
مشخصات يك View model خوب به شرح زير است:
الف) رابطه بين يك View و View model آن، رابطه‌اي يك به يك است. به ازاي هر View، بهتر است يك كلاس View model وجود داشته باشد.
ب) View ساختار View model را ديكته مي‌كند و نه كنترلر.
ج) View modelها صرفا يك سري كلاس POCO (كلاس‌هايي تشكيل شده از خاصيت، خاصيت، خاصيت ....) هستند كه هيچ منطقي در آن‌ها قرار نمي‌گيرد.
د) View model بايد حاوي تمام اطلاعاتي باشد كه View جهت رندر نياز دارد و نه بيشتر و الزامي هم ندارد كه اين اطلاعات مستقيما به domain models مرتبط شوند. براي مثال اگر قرار است firstName+LastName در View نمايش داده شود، كار اين جمع زدن بايد حين تهيه View Model انجام شود و نه داخل View. يا اگر قرار است اطلاعات عددي با سه رقم جدا كننده به كاربر نمايش داده شوند، وظيفه View Model است كه يك خاصيت اضافي را براي تهيه اين مورد تدارك ببيند. يا مثلا اگر يك فرم ثبت نام داريم و در اين فرم ليستي وجود دارد كه تنها Id عنصر انتخابي آن در Model اصلي مورد استفاده قرار مي‌گيرد، تهيه اطلاعات اين ليست هم كار ViewModel است و نه اينكه مدام به Model اصلي بخواهيم خاصيت اضافه كنيم.

ViewModel چگونه پياده سازي مي‌شود؟
اكثر مقالات را كه مطالعه كنيد، اين روش را توصيه مي‌كنند:

public class MyViewModel
{
    public SomeDomainModel1 Model1 { get; set; }
    public SomeDomainModel2 Model2 { get; set; }
    ...
}

يعني اينكه View ما به اطلاعات مثلا دو Model نياز دارد. اين‌ها را به اين شكل محصور و كپسوله مي‌كنيم. اگر View، واقعا به تمام فيلدهاي اين كلاس‌ها نياز داشته باشد، اين روش صحيح است. در غير اينصورت، اين روش نادرست است (و متاسفانه همه جا هم دقيقا به اين شكل تبليغ مي‌شود).
ViewModel محصور كننده يك يا چند مدل نيست. در اينجا حس غلط كار كردن با يك ViewModel را داريم. ViewModel فقط بايد ارائه كننده اطلاعاتي باشد كه يك View نياز دارد و نه بيشتر و نه تمام خواص تمام كلاس‌هاي تعريف شده. به عبارتي اين نوع تعريف صحيح است:

public class MyViewModel
{
    public string SomeExtraField1 { get; set; }
    public string SomeExtraField2 { get; set; }
    public IEnumerable<SelectListItem> StateSelectList { get; set; }
    // ...
    public string PersonFullName { set; set; }
}

در اينجا، View متناظري، قرار است نام كامل يك شخص را به علاوه يك سري اطلاعات اضافي كه در domain model نيست، نمايش دهد. مثلا نمايش نام استان‌ها كه نهايتا Id انتخابي آن قرار است در برنامه استفاده شود.
خلاصه علت وجودي ViewModel اين موارد است:
الف) Model برنامه را مستقيما در معرض استفاده قرار ندهيم (عدم رعايت اين نكته به مشكلات امنيتي حادي هم حين به روز رساني اطلاعات ممكن است ختم ‌شود كه پيشتر توضيح داده شد).
ب) فيلدهاي نمايشي اضافي مورد نياز يك View را داخل Model برنامه تعريف نكنيم (مثلا تعاريف عناصر يك دراپ داون ليست، جايش اينجا نيست. مدل فقط نياز به Id عنصر انتخابي آن دارد).

با اين توضيحات، اگر View به روز رساني اطلاعات كلمه عبور كاربر، تنها به اطلاعات id آن كاربر و كلمه عبور او نياز دارد، فقط بايد همين اطلاعات را در اختيار View قرار داد و نه بيشتر:

namespace MvcApplication7.Models
{
    public class UserViewModel
    {
        public int Id { set; get; }
        public string Password { set; get; }
    }
}

به اين ترتيب ديگر خاصيت IsAdming اضافه‌اي وجود ندارد كه بخواهد مورد حمله واقع شود.



استفاده از model binding براي آپلود فايل به سرور

براي آپلود فايل به سرور تنها كافي است يك اكشن متد به شكل زير را تعريف كنيم. HttpPostedFileBase نيز يكي ديگر از model binderهاي توكار ASP.NET MVC است:

[HttpGet]
public ActionResult Upload()
{
       return View(); // Shows the upload page
}

[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
       string filename = Server.MapPath("~/files/somename.ext");
       file.SaveAs(filename);
       return RedirectToAction("Index");
}

View متناظر هم مي‌تواند به شكل زير باشد:

@{
    ViewBag.Title = "Upload";
}
<h2>
    Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User", 
                       method: FormMethod.Post,
                       htmlAttributes: new { enctype = "multipart/form-data" }))
{
    <text>Upload a photo:</text> <input type="file" name="photo" />
    <input type="submit" value="Upload" />
}

اگر دقت كرده باشيد در طراحي ASP.NET MVC از anonymously typed objects زياد استفاده مي‌شود. در اينجا هم براي معرفي enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتي هر جايي كه مشخص نبوده چه تعداد ويژگي يا كلا چه ويژگي‌ها و خاصيت‌هايي را مي‌توان تنظيم كرد، اجازه تعريف آن‌ها را به صورت anonymously typed objects ميسر كرده‌اند. يك نمونه ديگر آن در متد routes.MapRoute فايل Global.asax.cs است كه پارامتر سوم دريافت مقدار پيش فرض‌ها نيز anonymously typed object است. يا نمونه ديگر آن‌را در همين قسمت در جايي كه لينك delete را به فرم تبديل كرديم مشاهده نموديد. مقدار routeValues هم يك anonymously typed object معرفي شد.



سفارشي سازي model binder پيش فرض ASP.NET MVC

در همين مثال فرض كنيد تاريخ را به صورت شمسي از كاربر دريافت مي‌كنيم. خاصيت تعريف شده هم DateTime ميلادي است. به عبارتي model binder حين تبديل رشته تاريخ شمسي دريافتي به تاريخ ميلادي با شكست مواجه شده و نهايتا خاصيت this.ModelState.IsValid مقدارش false خواهد بود. براي حل اين مشكل چكار بايد كرد؟
براي اين منظور بايد نحوه پردازش يك نوع خاص را سفارشي كرد. ابتدا با پياده سازي اينترفيس IModelBinder شروع مي‌كنيم. توسط bindingContext.ValueProvider مي‌توان به مقداري كه كاربر وارد كرده در ميانه راه دسترسي يافت. آن‌را تبديل كرده و نمونه صحيح را بازگشت داد.
نمونه‌اي از اين پياده سازي را در ادامه ملاحظه مي‌كنيد:

using System;
using System.Globalization;
using System.Web.Mvc;

namespace MvcApplication7.Binders
{
    public class PersianDateModelBinder : IModelBinder
    {

        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            var modelState = new ModelState { Value = valueResult };
            object actualValue = null;
            try
            {
                var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
                if (parts.Length != 3) return null;
                int year = int.Parse(parts[0]);
                int month = int.Parse(parts[1]);
                int day = int.Parse(parts[2]);
                actualValue = new DateTime(year, month, day, new PersianCalendar());
            }
            catch (FormatException e)
            {
                modelState.Errors.Add(e);
            }

            bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
            return actualValue;
        }
    }
}

سپس براي معرفي PersianDateModelBinder جديد تنها كافي است سطر زير را

ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());

به متد Application_Start قرار گرفته در فايل Global.asax.cs برنامه اضافه كرد. از اين پس كاربران مي‌توانند تاريخ‌ها را در برنامه شمسي وارد كنند و model binder بدون مشكل خواهد توانست اطلاعات ورودي را به معادل DateTime ميلادي آن تبديل كند و استفاده نمايد.
تعريف مدل بايندر سفارشي در فايل Global.asax.cs آن‌را به صورت سراسري در تمام مدل‌ها و اكشن‌متدها فعال خواهد كرد. اگر نياز بود تنها يك اكشن متد خاص از اين مدل بايندر سفارشي استفاده كند مي‌توان به روش زير عمل كرد:

public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)

همچنين ويژگي ModelBinder را به يك كلاس هم مي‌توان اعمال كرد:

[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{