۱۳۹۱/۰۲/۱۰

CheckBoxList در ASP.NET MVC


ASP.NET MVC به همراه HtmlHelper توكاري جهت نمايش يك ChekBoxList نيست؛ اما سيستم Model binder آن، اين نوع كنترل‌ها را به خوبي پشتيباني مي‌كند. براي مثال، يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس يك كنترلر Home جديد را نيز به آن اضافه كنيد. در ادامه، براي متد Index آن، يك View خالي را ايجاد نمائيد. سپس محتواي اين View را به نحو زير تغيير دهيد:
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
@using (Html.BeginForm())
{
    <input type='checkbox' name='Result' value='value1' />
    <input type='checkbox' name='Result' value='value2' />
    <input type='checkbox' name='Result' value='value3' />
    <input type="submit" value="submit" />
}

و كنترلر Home را نيز مطابق كدهاي زير ويرايش كنيد:
using System.Web.Mvc;

namespace MvcApplication21.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(string[] result)
        {
            return View();
        }
    }
}

يك breakpoint را در تابع Index دوم كه آرايه‌اي را دريافت مي‌كند، قرار دهيد. سپس برنامه را اجرا كرده، تعدادي از checkboxها را انتخاب و فرم نمايش داده شده را به سرور ارسال كنيد:


بله. همانطور كه ملاحظه مي‌كنيد، تمام عناصر ارسالي انتخاب شده كه داراي نامي مشابه بوده‌اند، به يك آرايه قابل بايند هستند و سيستم model binder مي‌داند كه چگونه بايد اين اطلاعات را دريافت و پردازش كند.
از اين مقدمه مي‌توان به عنوان پايه و اساس نوشتن يك HtmlHelper سفارشي CheckBoxList استفاده كرد.
براي اين منظور يك پوشه جديد را به نام app_code، به ريشه پروژه اضافه نمائيد. سپس يك فايل خالي را به نام Helpers.cshtml نيز به آن اضافه كنيد. محتواي اين فايل را به نحو زير تغيير دهيد:

@helper CheckBoxList(string name, List<System.Web.Mvc.SelectListItem> items)
    {
    <div class="checkboxList">
        @foreach (var item in items)
        {
            @item.Text
            <input type="checkbox" name="@name"
                   value="@item.Value"
                   @if (item.Selected) { <text>checked="checked"</text> }
               />
            < br />
        }
    </div>
}

و براي استفاده از آن، كنترلر Home را مطابق كدهاي زير ويرايش كنيد:

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

namespace MvcApplication21.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            ViewBag.Tags = new List<SelectListItem>
            {
                new SelectListItem { Text = "Item1", Value = "Val1", Selected = false },
                new SelectListItem { Text = "Item2", Value = "Val2", Selected = false },
                new SelectListItem { Text = "Item3", Value = "Val3", Selected = true }
            };
            return View();
        }

        [HttpPost]
        public ActionResult GetTags(string[] tags)
        {
            return View();
        }

        [HttpPost]
        public ActionResult Index(string[] result)
        {
            return View();
        }
    }
}

و در اين حالت View برنامه به شكل زير درخواهد آمد:
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
@using (Html.BeginForm())
{
    
    <input type='checkbox' name='Result' value='value1' />
    <input type='checkbox' name='Result' value='value2' />
    <input type='checkbox' name='Result' value='value3' />
    <input type="submit" value="submit" />
}

@using (Html.BeginForm(actionName: "GetTags", controllerName: "Home"))
{
    @Helpers.CheckBoxList("Tags", (List<SelectListItem>)ViewBag.Tags)    
    <input type="submit" value="submit" />
}

با توجه به اينكه كدهاي Razor قرار گرفته در پوشه خاص app_code در ريشه سايت، به صورت خودكار در حين اجراي برنامه كامپايل مي‌شوند، متد Helpers.CheckBoxList در تمام Viewهاي برنامه در دسترس خواهد بود. در اين متد، يك نام و ليستي از SelectListItemها دريافت مي‌گردد. سپس به صورت خودكار يك CheckboxList را توليد خواهد كرد. براي دريافت مقادير ارسالي آن به سرور هم بايد مطابق متد GetTags تعريف شده در كنترلر Home عمل كرد. در اينجا Value عناصر انتخابي به صورت آرايه‌اي از رشته‌ها در دسترس خواهد بود.

روشي جامع‌تر
در آدرس زير مي‌توانيد يك HtmlHelper بسيار جامع را جهت توليد CheckBoxList در ASP.NET MVC بيابيد. در همان صفحه روش استفاده از آن، به همراه چندين مثال ارائه شده است:
https://github.com/devnoob/MVC3-Html.CheckBoxList-custom-extension

۱۳۹۱/۰۲/۰۸

ASP.NET MVC #24


مروري بر نمونه سؤالات ASP.NET MVC امتحانات مايكروسافت در چند سال اخير

در قسمت آخر سري ASP.NET MVC بد نيست مروري داشته باشيم بر نمونه سؤالات امتحانات مايكروسافت؛ امتحانات 70-515 و 70-519 كه در آن‌ها تعدادي از سؤالات به ASP.NET MVC اختصاص دارند. در اين سؤالات امكان انتخاب بيش از يك گزينه نيز وجود دارد.


1) شما در حال توسعه يك برنامه‌ي ASP.NET MVC ‌هستيد. بايد درخواست Ajax ايي از صفحه‌اي صادر شده و خروجي زير را از اكشن متدي دريافت كند:
["Adventure Works","Contoso"] 

كدام نوع خروجي اكشن متد زير را براي اينكار مناسب مي‌دانيد؟
a) AjaxHelper 
b) XDocument 
c) JsonResult
d) DataContractJsonSerializer



2) شما در حال طراحي يك برنامه ASP.NET MVC ‌هستيد. محتواي يك View بايد بر اساس نيازمندي‌هاي زير تشكيل شود:
الف) ارائه محتواي رندر شده user controls/partial views به مرورگر
ب) كار انتخاب user controls/partial views مناسب در اكشن متد كنترلر بايد انجام شود
استفاده از كدام روش زير را توصيه مي‌كنيد؟
a) Use the Html.RenderPartial extension method
b) Use the Html.RenderAction extension method
c) Use the PartialViewResult class
d) Use the ContentResult class


3) در حين طراحي يك برنامه ASP.NET MVC، نياز است منطق مديريت استثناهاي رخ داده و همچنين ثبت وقايع مرتبط را در يك مكان يا كلاس مركزي مديريت كنيد. كدام روش زير را پيشنهاد مي‌دهيد؟
a) استفاده از try/catch در تمام متدها
b) تحريف متد OnException در كنترلرها
c) مزين سازي تمام كنترلرها به ويژگي HandleError سفارشي شده
d) مزين سازي تمام كنترلرها به ويژگي HandleError پيش فرض


4) شما در حال توزيع برنامه‌ي ASP.NET MVC خود جهت اجرا بر روي IIS 6.x هستيد. چه ملاحظاتي را بايد مدنظر داشته باشيد تا برنامه به درستي كار كند؟
a) تنظيم IIS به نحوي‌كه تمام درخواست‌ها را بر اساس wildcard‌ خاصي به aspnet_isapi.dll هدايت كند.
b) تنظيم IIS به نحوي‌كه تمام درخواست‌ها را بر اساس wildcard‌ خاصي به aspnet_wp.exe هدايت كند.
c) تغيير برنامه به نحوي‌كه تمام درخواست‌ها را به يك HttpHandler خاص هدايت كند.
d) تغيير برنامه به نحوي‌كه تمام درخواست‌ها را به يك HttpModule خاص هدايت كند.


5) شما در حال توسعه برنامه‌ي ASP.NET MVC هستيد كه در پوشه Views/Shared/DisplayTemplates آن، فايلي به نام score.cshtml به عنوان يك templated helper نمايش سفارشي اعداد صحيح تعريف شده است. مدل برنامه هم مطابق تعاريف زير است:
public class Player
{
   public String Name { get; set; }
   public int LastScore { get; set; }
   public int HighScore { get; set; }
}

در اينجا اگر نياز باشد تا فايل score.cshtml ياد شده به صورت خودكار به خاصيت LastScore در حين فراخواني متد HtmlHelper.DisplayForModel اعمال شود، چه روشي را پيشنهاد مي‌دهيد؟
a) فايل score.cshtml بايد به LastScore.cshtml تغيير نام يابد.
b) فايل ياد شده بايد از پوشه Views/Shared/DisplayTemplates به پوشه Views/Player/DisplayTemplates منتقل شود.
c) بايد از ويژگي UIHint به همراه مقدار score جهت مزين سازي خاصيت LastScore استفاده كرد.
[UIHint("Score")]
d) بايد از ويژگي زير براي مزين سازي خاصيت مورد نظر استفاده كرد:
[Display(Name="LastScore", ShortName="Score")]


6) شما در حال طراحي برنامه‌ي ASP.NET MVC هستيد كه در آن متد Edit كنترلري بايد تنها توسط كاربران اعتبارسنجي شده قابل دسترسي باشد. استفاده از كدام دو گزينه زير را براي اين منظور توصيه مي‌كنيد؟
a) [Authorize(Users = "")]
b) [Authorize(Roles = "")]
c) [Authorize(Users = "*")]
d) [Authorize(Roles = "*")]

7) قطعه كد HTML زير را درنظر بگيريد:
<span id="ref">
<a name=Reference>Check out</a>
the FAQ on
<a href="http://www.contoso.com">
Contoso</a>'s web site for more information:
<a href="http://www.contoso.com/faq">FAQ</a>.
</span>
<a href="http://www.contoso.com/home">Home</a>

قصد داريم به كمك jQuery در span ايي با id مساوي ref، متن تمام لينك‌ها را ضخيم كنيم. كدام گزينه زير را پيشنهاد مي‌دهيد؟

a) $("#ref").filter("a[href]").bold();
b) $("ref").filter("a").css("bold");
c) $("a").css({fontWeight:"bold"});
d) $("#ref a[href]").css({fontWeight:"bold"});


۱۳۹۱/۰۲/۰۷

ASP.NET MVC #23


اجراي برنامه‌هاي ASP.NET MVC توسط نگارش‌هاي متفاوت IIS

تا اينجا براي اجراي برنامه‌هاي ASP.NET MVC از وب سرور توكار VS.NET استفاده شد كه صرفا جهت آزمايش برنامه‌ها طراحي شده است. تا اين تاريخ سه رده از وب سرورهاي مايكروسافت ارائه شده‌اند كه براي نصب ASP.NET MVC مي‌توانند مورد استفاده قرار گيرند و هر كدام هم نكته‌هاي خاص خودشان را دارند كه در ادامه به بررسي آن‌ها خواهيم پرداخت.


اجراي برنامه‌هاي ASP.NET MVC بر روي IIS 5.x ويندوز XP

پس از ايجاد يك دايركتوري مجازي بر روي پوشه يك برنامه ASP.NET MVC و سعي در اجراي برنامه، بلافاصله پيغام خطاي HTTP 403 forbidden مشاهده مي‌شود.
اولين كاري كه براي رفع اين مساله بايد صورت گيرد، كليك راست بر روي نام دايركتوري مجازي در كنسول IIS، انتخاب گزينه خواص و سپس مراجعه به برگه «ASP.NET» آن است. در اينجا شماره نگارش دات نت فريم ورك مورد استفاده را به 4 تغيير دهيد (براي نمونه ASP.NET MVC 3.0 مبتني بر دات نت فريم ورك 4 است).
بعد از اين تغيير، بازهم موفق به اجراي برنامه‌هاي ASP.NET MVC بر روي IIS 5.x نخواهيم شد؛ چون در آن زمان مفاهيم مسيريابي و Routing كه اصل و پايه ASP.NET MVC هستند وجود خارجي نداشتند. اين نگارش از IIS به صورت پيش فرض تنها قادر به پردازش درخواست‌هاي رسيده‌اي كه به يك فايل فيزيكي بر روي سرور اشاره مي‌كند، مي‌باشد (يعني مشكلي با اجراي برنامه‌هاي ASP.NET Web forms ندارد).
براي رفع اين مشكل، مجددا بر روي نام دايركتوري مجازي برنامه در كنسول IIS كليك راست كرده و گزينه خواص را انتخاب كنيد. در صفحه ظاهر شده، در برگه «Virtual directory» آن، بر روي دكمه «Configuration» كليك نمائيد. در صفحه باز شده مجددا بر روي دكمه «Add» كليك كنيد.
در صفحه باز شده، مسير Executable را C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll وارد كرده و Extension را به .* (دات هرچي) تنظيم كنيد. همين مقدار تنظيم، براي اجراي برنامه‌هاي ASP.NET MVC بر روي IIS 5.x ويندوز XP كفايت مي‌كند.

كاري كه در اينجا انجام شده است، نگاشت تمام درخواست‌هاي رسيده صرفنظر از پسوند فايل‌ها، به موتور ASP.NET مي‌باشد. به صورت پيش فرض در IIS 5.x درخواست‌ها تنها بر اساس پسوند فايل‌ها پردازش مي‌شوند. مثلا اگر فايل درخواستي aspx است، درخواست رسيده به aspnet_isapi.dll ياد شده هدايت خواهد شد. اگر پسوند فايل php است به isapi مخصوص آن (در صورت نصب) هدايت مي‌گردد و به همين ترتيب براي ساير سيستم‌هاي ديگر. زمانيكه Extension به «دات هرچي» و Executable به aspnet_isapi.dll دات نت 4 تنظيم مي‌شود، دايركتوري مجازي تنظيم شده تنها جهت سرويس دهي به يك برنامه ASP.NET عمل خواهد كرد و تمام درخواست‌هاي رسيده به آن، به موتور اجرايي ASP.NET هدايت مي‌شوند.

بديهي است تنظيمات فوق تنها به يك دايركتوري مجازي اعمال شدند. اگر نياز باشد تا بر روي تمام سايت‌ها تاثير گذار شود، اينبار در كنسول IIS 5.x بر روي «Default web site» كليك راست كرده و گزينه خواص را انتخاب كنيد. در صفحه باز شده به برگه «Home directory» مراجعه كرده و مراحل ذكر شده را تكرار كنيد.

مشكل! اين روش بهينه نيست.
روش فوق خوبه، كار مي‌كنه، اما بهينه نيست؛ از اين جهت كه «نگاشت تمام درخواست‌ها به موتور ASP.NET» يعني پروسه پردازش درخواست يك فايل تصويري، js يا css هم بايد از فيلتر موتور ASP.NET عبور كند كه ضروري نيست.
براي رفع اين مشكل، توصيه شده است كه سيستم مسيريابي ASP.NET MVC را در IIS 5.x «پسوند دار» كنيد. به اين نحو كه با مراجعه به فايل Global.asax.cs، تعاريف مسيريابي را به نحو زير ويرايش كنيد:

public static void RegisterRoutes(RouteCollection routes)
{
       routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
       routes.Add(
                new Route("{controller}.aspx/{action}/{id}", new MvcRouteHandler()) 
                {
                    Defaults = new RouteValueDictionary(new 
                    {
                        controller = "Home",
                        action = "Index",
                        id = UrlParameter.Optional
                    }) 
                });



اينبار براي مثال مسير http://localhost/MyMvcApp/home.aspx/index به علت داشتن پسوند aspx وارد موتور پردازشي ASP.NET خواهد شد. البته در اين حالت URL هاي تميز ASP.NET MVC را از دست خواهيم داد و مدام بايد دقت داشت كه مسيرهاي كنترلرها حتما بايد به aspx ختم شوند. ضمنا با اين تنظيم، ديگر نيازي به تغيير تعاريف نگاشت‌ها در كنسول مديريتي IIS، نخواهد بود.


اجراي برنامه‌هاي ASP.NET MVC بر روي IIS 6.x ويندوز سرور 2003

تمام نكات عنوان شده جهت IIS 5.x در IIS 6.x نيز صادق هستند. به علاوه براي اجراي برنامه‌هاي ASP.NET بر روي IIS 6.x بايد به دو نكته مهم ديگر نيز دقت داشت:
الف) ASP.NET 4 به صورت پيش فرض در IIS 6.x غيرفعال است كه بايد با مراجعه به قسمت Web Services Extensions در كنسول مديريتي IIS، آن‌را از حالت prohibited خارج كرد.
ب) در هر Application pool تنها از يك نگارش دات نت فريم ورك مي‌توان استفاده كرد. براي مثال اگر هم اكنون AppPool1 مشغول سرويس دهي به يك سايت ASP.NET 3.5 است، از آن نمي‌توانيد جهت اجراي برنامه‌هاي ASP.NET MVC 3 به بعد استفاده كنيد. زيرا براي مثال ASP.NET MVC 3 مبتني بر دات نت فريم ورك 4 است. به همين جهت حتما نياز است تا يك Application pool مجزا را براي برنامه‌هاي دات نت 4 در IIS 6 اضافه نمائيد و سپس در تنظيمات سايت، از اين Application pool جديد استفاده نمائيد.
البته روش صحيح و اصولي كار با IIS از نگارش 6 به بعد هم مطابق شرحي است كه عنوان شد. براي دستيابي به بهترين كارآيي و امنيت بيشتر، بهتر است به ازاي هر سايت، از يك Application pool مجزا استفاده نمائيد.

اطلاعات تكميلي:
نکات نصب برنامه‌هاي ASP.NET 4.0 بر روي IIS 6
مروري بر تاريخچه محدوديت حافظه مصرفي برنامه‌هاي ASP.NET در IIS



اجراي برنامه‌هاي ASP.NET MVC بر روي IIS 7.x ويندوز 7 و ويندوز سرور 2008

اگر برنامه ASP.NET MVC در IIS 7.x در حالت يكپارچه (integrated mode) اجرا شود، بدون نياز به هيچگونه تغييري در تنظيمات سرور يا برنامه، بدون مشكل قابل اجرا خواهد بود. بديهي است در اينجا نيز بهتر است به ازاي هر برنامه، يك Application pool مجزا را ايجاد كرد.
اما در حالت classic (كه براي برنامه‌هاي جديد توصيه نمي‌شود) نياز است همان مراحل IIS 5,x تكرار شود. البته اينبار مسير زير را بايد طي كرد تا به صفحه افزودن نگاشت‌ها رسيد:
Right-click on a web site -> Properties -> Home Directory tab -> click on the Configuration button -> Mappings tab



نكته‌اي مهم در تمام نگارش‌هاي IIS

ترتيب نصب دات نت فريم ورك 4 و IIS مهم است. اگر ابتدا IIS نصب شود و سپس دات نت فريم ورك 4، به صورت خودكار، كار نگاشت اطلاعات ASP.NET به IIS صورت خواهد گرفت.
اگر ابتدا دات نت فريم ورك 4 نصب شود و سپس IIS، براي مثال ديگر از برگه ASP.NET در IIS 6.x خبري نخواهد بود. براي رفع اين مشكل دستور زير را در خط فرمان اجرا كنيد:

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\aspnet_regiis.exe /i

به اين ترتيب، اطلاعات مرتبط با موتور ASP.NET مجددا به تنظيمات IIS اضافه خواهند شد.


۱۳۹۱/۰۲/۰۶

ASP.NET MVC #22


تهيه سايت‌هاي چند زبانه و بومي سازي نمايش اطلاعات در ASP.NET MVC

زمانيكه دات نت فريم ورك نياز به انجام اعمال حساس به مسايل بومي را داشته باشد،‌ ابتدا به مقادير تنظيم شده دو خاصيت زير دقت مي‌كند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر اين اساس دات نت مي‌تواند تشخيص دهد كه براي مثال خروجي متد DateTime.Now.ToString در كانادا و آمريكا بايد با هم تفاوت داشته باشند. مثلا در آمريكا ابتدا ماه، سپس روز و در آخر سال نمايش داده مي‌شود و در كانادا ابتدا سال، بعد ماه و در آخر روز نمايش داده خواهد شد. يا نمونه‌ي ديگري از اين دست مي‌تواند نحوه نمايش علامت واحد پولي كشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روي بارگذاري فايل‌هاي مخصوصي به نام Resource، تاثير گذار است.

اين خواص را يا به صورت دستي مي‌توان تنظيم كرد و يا ASP.NET، اين اطلاعات را از هدر Accept-Language دريافتي از مرورگر كاربر به صورت خودكار مقدار دهي مي‌كند. البته براي اين منظور نياز است يك سطر زير را به فايل وب كانفيگ برنامه اضافه كرد:

<system.web>
    <globalization culture="auto" uiCulture="auto" />

يا اگر نياز باشد تا برنامه را ملزم به نمايش اطلاعات Resource مرتبط با فرهنگ بومي خاصي كرد نيز مي‌توان در همين قسمت مقادير culture و uiCulture را دستي تنظيم نمود و يا اگر همانند برنامه‌هايي كه چند لينك را بالاي صفحه نمايش مي‌دهند كه براي مثال به نگارش‌هاي فارسي/عربي/انگليسي اشاره مي‌كند، اينكار را با كد نويسي نيز مي‌توان انجام داد:

System.Threading.Thread.CurrentThread.CurrentCulture =
                                System.Globalization.CultureInfo.CreateSpecificCulture("fa");


جهت آزمايش اين مطلب، ابتدا تنظيم globalization فوق را به فايل وب كانفيگ برنامه اضافه كنيد. سپس به مسير زير در IE مراجعه كنيد:

IE -> Tools -> Internet options -> General tab -> Languages

در اينجا مي‌توان هدر Accept-Language را مقدار دهي كرد. براي نمونه اگر مقدار زبان پيش فرض را به فرانسه تنظيم كنيم (به عنوان اولين زبان تعريف شده در ليست) و سپس سعي در نمايش مقدار decimal زير را داشته باشيم:

string.Format("{0:C}", 10.5M)

اگر زبان پيش فرض، انگليسي آمريكايي باشد، $ نمايش داده خواهد شد و اگر زبان به فرانسه تنظيم شود، يورو در كنار عدد مبلغ نمايش داده مي‌شود.
تا اينجا تنها با تنظيم culture=auto به اين نتيجه رسيده‌ايم. اما ساير قسمت‌هاي صفحه چطور؟ براي مثال برچسب‌هاي نمايش داده شده را چگونه مي‌توان به صورت خودكار بر اساس Accept-Language مرجح كاربر تنظيم كرد؟ خوشبختانه در دات نت، زير ساخت مديريت برنامه‌هاي چند زبانه به صورت توكار وجود دارد كه در ادامه به بررسي آن خواهيم پرداخت.


آشنايي با ساختار فايل‌هاي Resource


فايل‌هاي Resource يا منبع، در حقيقت فايل‌هايي هستند مبتني بر XML با پسوند resx و هدف آن‌ها ذخيره سازي رشته‌هاي متناظر با فرهنگ‌هاي مختلف مي‌باشد و براي استفاده از آن‌ها حداقل يك فايل منبع پيش فرض بايد تعريف شود. براي نمونه فايل mydata.resx را در نظر بگيريد. براي ايجاد فايل منبع اسپانيايي متناظر، بايد فايلي را به نام mydata.es.resx توليد كرد. البته نوع فرهنگ مورد استفاده را كاملتر نيز مي‌توان ذكر كرد براي مثال mydata.es-mex.resx جهت فرهنگ اسپانيايي مكزيكي بكارگرفته خواهد شد، يا mydata.fr-ca.resx به فرانسوي كانادايي اشاره مي‌كند. سپس مديريت منابع دات نت فريم ورك بر اساس مقدار CurrentUICulture جاري، اطلاعات فايل متناظري را بارگذاري خواهد كرد. اگر فايل متناظري وجود نداشت، از اطلاعات همان فايل پيش فرض استفاده مي‌گردد.
حين تهيه برنامه‌ها نيازي نيست تا مستقيما با فايل‌هاي XML منابع كار كرد. زمانيكه اولين فايل منبع توليد مي‌شود، به همراه آن يك فايل cs يا vb نيز ايجاد خواهد شد كه امكان دسترسي به كليدهاي تعريف شده در فايل‌هاي XML را به صورت strongly typed ميسر مي‌كند. اين فايل‌هاي خودكار، تنها براي فايل پيش فرض mydata.resx توليد مي‌شوند،‌از اين جهت كه تعاريف اطلاعات ساير فرهنگ‌هاي متناظر نيز بايد با همان كليدهاي فايل پيش فرض آغاز شوند. تنها «مقادير» كليدهاي تعريف شده در كلاس‌هاي منبع متفاوت هستند.
اگر به خواص فايل‌هاي resx در VS.NET دقت كنيم، نوع Build action آن‌ها به embedded resource تنظيم شده است.


مثالي جهت بررسي استفاده از فايل‌هاي Resource

يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. فايل وب كانفيگ آن‌را ويرايش كرده و تنظيمات globalization ابتداي بحث را به آن اضافه كنيد. سپس مدل، كنترلر و View متناظر با متد Index آن‌را با محتواي زير به پروژه اضافه نمائيد:

namespace MvcApplication19.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

using System.Web.Mvc;
using MvcApplication19.Models;

namespace MvcApplication19.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var employee = new Employee { Name = "Name 1" };
            return View(employee);
        }
    }
}

@model MvcApplication19.Models.Employee
@{
    ViewBag.Title = "Index";
}
<h2>
    Index</h2>
<fieldset>
    <legend>Employee</legend>
    <div class="display-label">
        Name
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>
</fieldset>
<fieldset>
    <legend>Employee Info</legend>
    @Html.DisplayForModel()
</fieldset>

قصد داريم در View فوق بر اساس uiCulture كاربر مراجعه كننده به سايت، برچسب Name را مقدار دهي كنيم. اگر كاربري از ايران مراجعه كند، «نام كارمند» نمايش داده شود و ساير كاربران، «Employee Name» را مشاهده كنند. همچنين اين تغييرات بايد بر روي متد Html.DisplayForModel نيز تاثيرگذار باشد.
براي اين منظور بر روي پوشه Views/Home كه محل قرارگيري فايل Index.cshtml فوق است كليك راست كرده و گزينه Add|New Item را انتخاب كنيد. سپس در صفحه ظاهر شده، گزينه «Resources file» را انتخاب كرده و براي مثال نام Index_cshtml.resx را وارد كنيد.
به اين ترتيب اولين فايل منبع مرتبط با View جاري كه فايل پيش فرض نيز مي‌باشد ايجاد خواهد شد. اين فايل، به همراه فايل Index_cshtml.Designer.cs توليد مي‌شود. سپس همين مراحل را طي كنيد، اما اينبار نام Index_cshtml.fa.resx را حين افزودن فايل منبع وارد نمائيد كه براي تعريف اطلاعات بومي ايران مورد استفاده قرار خواهد گرفت. فايل دومي كه اضافه شده است، فاقد فايل cs همراه مي‌باشد.
اكنون فايل Index_cshtml.resx را در VS.NET باز كنيد. از بالاي صفحه، به كمك گزينه Access modifier، سطح دسترسي متدهاي فايل cs همراه آن‌را به public تغيير دهيد. پيش فرض آن internal است كه براي كار ما مفيد نيست. از اين جهت كه امكان دسترسي به متدهاي استاتيك تعريف شده در فايل خودكار Index_cshtml.Designer.cs را در View هاي برنامه، نخواهيم داشت. سپس دو جفت «نام-مقدار» را در فايل resx وارد كنيد. مثلا نام را Name و مقدار آن‌را «Employee Name» و سپس نام ديگر را NameIsNotRight و مقدار آن‌را «Name is required» وارد نمائيد.
در ادامه فايل Index_cshtml.fa.resx را باز كنيد. در اينجا نيز دو جفت «نام-مقدار» متناظر با فايل پيش فرض منبع را بايد وارد كرد. كليدها يا نام‌ها يكي است اما قسمت مقدار اينبار بايد فارسي وارد شود. مثلا نام را Name و مقدار آن‌را «نام كارمند» وارد نمائيد. سپس كليد يا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائيد» را تنظيم نمائيد.
تا اينجا كار تهيه فايل‌هاي منبع متناظر با View جاري به پايان مي‌رسد.
در ادامه با كمك فايل Index_cshtml.Designer.cs كه هربار پس از تغيير فايل resx متناظر آن به صورت خودكار توسط VS.NET توليد و به روز مي‌شود، مي‌توان به كليدها يا نام‌هايي كه تعريف كرده‌ايم، در قسمت‌هاي مختلف برنامه دست يافت. براي نمونه تعريف كليد Name در اين فايل به نحو زير است:

namespace MvcApplication19.Views.Home {
    public class Index_cshtml {
        public static string Name {
            get {
                return ResourceManager.GetString("Name", resourceCulture);
            }
        }
    }
}

بنابراين براي استفاده از آن در هر View ايي تنها كافي است بنويسيم:

@MvcApplication19.Views.Home.Index_cshtml.Name

به اين ترتيب بر اساس تنظيمات محلي كاربر، اطلاعات به صورت خودكار از فايل‌هاي Index_cshtml.fa.resx فارسي يا فايل پيش فرض Index_cshtml.resx، دريافت مي‌گردد.
علاوه بر امكان دسترسي مستقيم به كليدهاي تعريف شده در فايل‌هاي منبع، امكان استفاده از آن‌ها توسط data annotations نيز ميسر است. در اين حالت مي‌توان مثلا پيغام‌هاي اعتبار سنجي را بومي كرد يا حين استفاده از متد Html.DisplayForModel، بر روي برچسب نمايش داده شده خودكار، تاثير گذار بود. براي اينكار بايد اندكي مدل برنامه را ويرايش كرد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication19.Models
{
    public class Employee
    {
        [ScaffoldColumn(false)]
        public int Id { set; get; }

        [Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
                 Name = "Name")]
        [Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
                  ErrorMessageResourceName = "NameIsNotRight")]
        public string Name { set; get; }
    }
}

همانطور كه ملاحظه مي‌كنيد، حين تعريف ويژگي‌هاي Display يا Required، امكان تعريف نام كلاس متناظر با فايل resx خاصي وجود دارد. به علاوه ErrorMessageResourceName به نام يك كليد در اين فايل و يا پارامتر Name ويژگي Display نيز به نام كليدي در فايل منبع مشخص شده، اشاره مي‌كنند. اين اطلاعات توسط متدهاي Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودكار مورد استفاده قرار خواهند گرفت.


نكته‌اي در مورد كش كردن اطلاعات
در اين مثال اگر فيلتر OutputCache را بر روي متد Index تعريف كنيم، حتما نياز است به هدر Accept-Language نيز دقت داشت. در غيراينصورت تمام كاربران، صرفنظر از تنظيمات بومي آن‌ها، يك صفحه را مشاهده خواهند كرد:

[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()


۱۳۹۱/۰۲/۰۵

ASP.NET MVC #21


آشنايي با تكنيك‌هاي Ajax در ASP.NET MVC

اهميت آشنايي با Ajax، ارائه تجربه‌ كاربري بهتري از برنامه‌هاي وب، به مصرف كنندگان نهايي آن مي‌باشد. به اين ترتيب مي‌توان درخواست‌هاي غيرهمزماني (asynchronous) را با فرمت XML يا Json به سرور ارسال كرد و سپس نتيجه نهايي را كه حجم آن نسبت به يك صفحه كامل بسيار كمتر است، به كاربر ارائه داد. غيرهمزمان بودن درخواست‌ها سبب مي‌شود تا ترد اصلي رابط كاربري برنامه قفل نشده و كاربر در اين بين مي‌تواند به ساير امور خود بپردازد. به اين ترتيب مي‌توان برنامه‌هاي وبي را كه شبيه به برنامه‌هاي دسكتاپ هستند توليد نمود؛ كل صفحه مرتبا به سرور ارسال نمي‌شود، flickering و چشمك زدن صفحه كاهش خواهد يافت (چون نيازي به ترسيم مجدد كل صفحه نخواهد بود و عموما قسمتي جزئي از يك صفحه به روز مي‌شود) يا بدون نياز به ارسال كل صفحه به سرور، به كاربري خواهيم گفت كه آيا اطلاعاتي كه وارد كرده است معتبر مي‌باشد يا نه (نمونه‌اي از آن‌ را در قسمت Remote validation اعتبار سنجي اطلاعات ملاحظه نموديد).


مروري بر محتويات پوشه Scripts يك پروژه جديد ASP.NET MVC در ويژوال استوديو

با ايجاد هر پروژه ASP.NET MVC‌ جديدي در ويژوال استوديو، يك سري اسكريپت‌ هم به صورت خودكار در پوشه Scripts آن اضافه مي‌شوند. تعدادي از اين فايل‌ها توسط مايكروسافت پياده سازي شده‌اند. براي مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js

اين فايل‌ها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاري با نگارش‌هاي قبلي قرار دارند و استفاده از آن‌ها اختياري است. بنابراين با خيال راحت آن‌ها را delete كنيد! روش توصيه شده جهت پياده سازي ويژگي‌هاي Ajax ايي، استفاده از كتابخانه‌هاي مرتبط با jQuery مي‌باشد؛ از اين جهت كه 100ها افزونه براي كار با آن توسط گروه وسيعي از برنامه نويس‌ها در سراسر دنيا تاكنون تهيه شده است. به علاوه فريم ورك jQuery تنها منحصر به اعمال Ajax ايي نيست و از آن جهت دستكاري DOM (document object model) و CSS صفحه نيز مي‌توان استفاده كرد. همچنين حجم كمي نيز داشته،‌ با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز مي‌شود.

در اين پوشه سه فايل ديگر پايه كتابخانه jQuery نيز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js

فايل vsdoc براي ارائه نهايي برنامه طراحي نشده است. هدف از آن ارائه Intellisense بهتري از jQuery در VS.NET مي‌باشد. فايلي كه بايد به كلاينت ارائه شود، فايل min يا فشرده شده آن است. اگر به آن نگاهي بيندازيم به نظر obfuscated مشاهده مي‌شود. علت آن هم حذف فواصل، توضيحات و همچنين كاهش طول متغيرها است تا اندازه فايل نهايي به حداقل خود كاهش پيدا كند. البته اين فايل از ديدگاه مفسر جاوا اسكريپت يك مرورگر، فايل بي‌نقصي است!
اگر علاقمند هستيد كه سورس اصلي jQuery را مطالعه كنيد، به فايل jquery-xyz.js مراجعه نمائيد.
محل الحاق اسكريپت‌هاي عمومي مورد نياز برنامه نيز بهتر است در فايل master page يا layout برنامه باشد كه به صورت پيش فرض اينكار انجام شده است.
ساير فايل‌هاي اسكريپتي كه در اين پوشه مشاهده مي‌شوند، يك سري افزونه عمومي يا نوشته شده توسط تيم ASP.NET MVC برفراز jQuery هستند.

به چهار نكته نيز حين استفاده از اسكريپت‌هاي موجود بايد دقت داشت:
الف) هميشه از متد Url.Content همانند تعاريفي كه در فايل Views\Shared\_Layout.cshtml مشاهده مي‌كنيد،‌ براي مشخص سازي مسير ريشه سايت، استفاده نمائيد. به اين ترتيب صرفنظر از آدرس جاري صفحه، همواره آدرس صحيح قرارگيري پوشه اسكريپت‌ها در صفحه ذكر خواهد شد.
ب) ترتيب فايل‌هاي js مهم هستند. ابتدا بايد كتابخانه اصلي jQuery ذكر شود و سپس افزونه‌هاي آن‌ها.
ج) اگر اسكريپت‌هاي jQuery در فايل layout سايت تعريف شده‌اند؛ نيازي به تعريف مجدد آن‌ها در View‌هاي سايت نيست.
د) اگر View ايي به اسكريپت ويژه‌اي جهت اجرا نياز دارد، بهتر است آن‌را به شكل يك section داخل view تعريف كرد و سپس به كمك متد RenderSection اين قسمت را در layout سايت مقدار دهي نمود. مثالي از آن‌را در قسمت 20 اين سري مشاهده نموديد (افزودن نمايش جمع هر ستون گزارش).


يك نكته
اگر آخرين به روز رساني‌هاي ASP.NET MVC را نيز نصب كرده باشيد، فايلي به نام packages.config به صورت پيش فرض به هر پروژه جديد ASP.NET MVC اضافه مي‌شود. به اين ترتيب VS.NET به كمك NuGet اين امكان را خواهد يافت تا شما را از آخرين به روز رساني‌هاي اين كتابخانه‌ها مطلع كند.


آشنايي با Ajax Helpers توكار ASP.NET MVC

اگر به تعاريف خواص و متدهاي كلاس WebViewPage دقت كنيم:

using System;

namespace System.Web.Mvc
{
    public abstract class WebViewPage<TModel> : WebViewPage
    {
        protected WebViewPage();
        public AjaxHelper<TModel> Ajax { get; set; }
        public HtmlHelper<TModel> Html { get; set; }
        public TModel Model { get; }
        public ViewDataDictionary<TModel> ViewData { get; set; }
        public override void InitHelpers();
        protected override void SetViewData(ViewDataDictionary viewData);
    }
}

علاوه بر خاصيت Html كه وهله‌اي از آن امكان دسترسي به Html helpers توكار ASP.NET MVC را در يك View فراهم مي‌كند، خاصيتي به نام Ajax نيز وجود دارد كه توسط آن مي‌توان به تعدادي متد AjaxHelper توكار دسترسي داشت. براي مثال توسط متد Ajax.ActionLink مي‌توان قسمتي از صفحه را به كمك ويژگي‌هاي Ajax، به روز رساني كرد.


مثالي در مورد به روز رساني قسمتي از صفحه به كمك متد Ajax.ActionLink

ابتدا نياز است فايل Views\Shared\_Layout.cshtml را اندكي ويرايش كرد. براي اين منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فايل layout برنامه اضافه نمائيد (اگر اين سطر اضافه نشود، متد Ajax.ActionLink همانند يك لينك معمولي رفتار خواهد كرد):

<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>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>    
</head>

سپس مدل ساده و منبع داده زير را نيز به پروژه اضافه كنيد:

namespace MvcApplication18.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

using System.Collections.Generic;

namespace MvcApplication18.Models
{
    public static class EmployeeDataSource
    {
        public static IList<Employee> CreateEmployees()
        {
            var list = new List<Employee>();
            for (int i = 0; i < 1000; i++)
            {
                list.Add(new Employee { Id = i + 1, Name = "name " + i });
            }
            return list;
        }
    }
}

در ادامه كنترلر جديدي را به برنامه با محتواي زير اضافه كنيد:

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

namespace MvcApplication18.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost] //for IE-8        
        public ActionResult EmployeeInfo(int? id)
        {
            if (!Request.IsAjaxRequest())
                return View("Error");

            if (!id.HasValue)
                return View("Error");

            var list = EmployeeDataSource.CreateEmployees();
            var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
            if (data == null)
                return View("Error");

            return PartialView(viewName: "_EmployeeInfo", model: data);
        }
    }
}

بر روي متد Index كليك راست كرده و گزينه Add view را انتخاب كنيد. يك View خالي را به آن اضافه نمائيد. همچنين بر روي متد EmployeeInfo كليك راست كرده و با انتخاب گزينه Add view در صفحه ظاهر شده يك partial view را اضافه نمائيد. جهت تمايز بين partial view و view هم بهتر است نام partial view با يك underline شروع شود.
كدهاي partial view مورد نظر را به نحو زير تغيير دهيد:

@model MvcApplication18.Models.Employee

<strong>Name:</strong> @Model.Name

سپس كدهاي View متناظر با متد Index را نيز به صورت زير اعمال كنيد:

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

<div id="EmployeeInfo">
    @Ajax.ActionLink(
     linkText: "Get Employee-1 info",
     actionName: "EmployeeInfo",
     controllerName: "Home",
     routeValues: new { id = 1 },
     ajaxOptions: new AjaxOptions
                  {
                      HttpMethod = "POST",
                      InsertionMode = InsertionMode.Replace,
                      UpdateTargetId = "EmployeeInfo",
                      LoadingElementId = "Progress"
                  })
</div>

<div id="Progress" style="display: none">
    <img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..."  />
</div>

توضيحات جزئيات كدهاي فوق

متد Ajax.ActionLink لينكي را توليد مي‌كند كه با كليك كاربر بر روي آن، اطلاعات اكشن متد واقع در كنترلري مشخص، به كمك ويژگي‌هاي jQuery Ajax دريافت شده و سپس در مقصدي كه توسط UpdateTargetId مشخص مي‌گردد، بر اساس مقدار InsertionMode،‌ درج خواهد شد (مي‌تواند قبل از آن درج شود يا پس از آن و يا اينكه كل محتواي مقصد را بازنويسي كند). HttpMethod آن هم به POST تنظيم شده تا با IE‌ مشكلي نباشد. از اين جهت كه IE پيغام‌هاي GET را كش مي‌كند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه يكي ديگر از خواص كلاس AjaxOptions، براي معرفي حالت بروز خطايي در سمت سرور به نام OnFailure در نظر گرفته شده است. در اينجا مي‌توان نام يك متد JavaScript ايي را مشخص كرده و پيغام خطاي عمومي را در صورت فراخواني آن به كاربر نمايش داد. يا توسط خاصيت Confirm آن مي‌توان يك پيغام را پيش از ارسال اطلاعات به سرور به كاربر نمايش داد.
به اين ترتيب در مثال فوق، id=1 به متد EmployeeInfo به صورت غيرهمزمان ارسال مي‌گردد. سپس كارمندي بر اين اساس يافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات كاربر مذكور، رندر خواهد شد. نتيجه كار، در يك div با id مساوي EmployeeInfo درج مي‌گردد (InsertionMode.Replace). متد Ajax.ActionLink از اين جهت داخل div تعريف شده‌است كه پس از كليك كاربر و جايگزيني محتوا، محو شود. اگر نيازي به محو آن نبود، آن‌را خارج از div تعريف كنيد.
عمليات دريافت اطلاعات از سرور ممكن است مدتي طول بكشد (براي مثال دريافت اطلاعات از بانك اطلاعاتي). به همين جهت بهتر است در اين بين از تصاويري كه نمايش دهنده انجام عمليات است، استفاده شود. براي اين منظور يك div با id مساوي Progress تعريف شده و id آن به LoadingElementId انتساب داده شده است. اين div با توجه به display: none آن، در ابتداي امر به كاربر نمايش داده نخواهد شد؛ در آغاز كار دريافت اطلاعات از سرور توسط متد Ajax.ActionLink نمايان شده و پس از خاتمه كار مجددا مخفي خواهد شد.
به علاوه اگر به كدهاي فوق دقت كرده باشيد، از متد Request.IsAjaxRequest نيز استفاده شده است. به اين ترتيب مي‌توان تشخيص داد كه آيا درخواست رسيده از طرف jQuery Ajax صادر شده است يا خير. البته آنچنان روش قابل ملاحظه‌اي نيست؛ چون امكان دستكاري Http Headers هميشه وجود دارد؛ اما بررسي آن ضرري ندارد. البته اين نوع بررسي‌ها را در ASP.NET MVC بهتر است تبديل به يك فيلتر سفارشي نمود؛ به اين ترتيب حجم if و else نويسي در متدهاي كنترلرها به حداقل خواهد رسيد. براي مثال:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    if (filterContext.HttpContext.Request.IsAjaxRequest())
    {
      base.OnActionExecuting(filterContext);
    }
    else
    {
      throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
    }
  }
}

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

[AjaxOnly]
public ActionResult SomeAjaxAction()
{
    return Content("Hello!");
}


در مورد كلمه unobtrusive در قسمت بررسي نحوه اعتبار سنجي اطلاعات، توضيحاتي را ملاحظه نموده‌ايد. در اينجا نيز از ويژگي‌هاي data-* براي معرفي پارامترهاي مورد نياز حين ارسال اطلاعات به سرور، استفاده مي‌گردد. براي مثال خروجي متد Ajax.ActionLink به شكل زير است. به اين ترتيب امكان حذف كدهاي جاوا اسكريپت از صفحه فراهم مي‌شود و توسط يك فايل jquery.unobtrusive-ajax.min.js كه توسط تيم ASP.NET MVC تهيه شده، اطلاعات مورد نياز به سرور ارسال خواهد گرديد:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" 
    data-ajax-mode="replace" data-ajax-update="#EmployeeInfo" 
    href="/Home/EmployeeInfo/1">Get Employee-1 info</a>

در كل اين روش قابليت نگهداري بهتري نسبت به روش اسكريپت نويسي مستقيم داخل صفحات را به همراه دارد. به علاوه جدا سازي افزونه اسكريپت وفق دهنده اين اطلاعات با متد jQuery.Ajax از صفحه جاري، كه امكان كش شدن آن‌را به سادگي ميسر مي‌سازد.


به روز رساني اطلاعات قسمتي از صفحه بدون استفاده از متد Ajax.ActionLink

الزامي به استفاده از متد Ajax.ActionLink و فايل jquery.unobtrusive-ajax.min.js وجود ندارد. اينكار را مستقيما به كمك jQuery نيز مي‌توان به نحو زير انجام داد:

<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
    <script type="text/javascript">
        function LoadEmployeeInfo() {
            showProgress();
            $.ajax({
                type: "POST",
                url: "/Home/EmployeeInfo",
                data: JSON.stringify({ id: 1 }),
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                // controller is returning a simple text, not json
                complete: function (xhr, status) {                    
                    var data = xhr.responseText;
                    if (status === 'error' || !data) {
                        //handleError                        
                    }
                    else {
                        $('#EmployeeInfo').html(data);
                    }
                    hideProgress();
                }
            });
        }
        function showProgress() {
            $('#Progress').css("display", "block");
        }
        function hideProgress() {
            $('#Progress').css("display", "none");
        }
    </script>
}

توضيحات:
توسط متد jQuery.Ajax نيز مي‌توان درخواست‌هاي Ajax ايي خود را به سرور ارسال كرد. در اينجا type نوع http verb مورد نظر را مشخص مي‌كند كه به POST تنظيم شده است. Url آدرس كنترلر را دريافت مي‌كند. البته حين استفاده از متد توكار Ajax.ActionLink،‌ اين لينك به صورت خودكار بر اساس تعاريف مسيريابي برنامه تنظيم مي‌شود. اما در صورت استفاده مستقيم از jQuery.Ajax بايد دقت داشت كه با تغيير تعاريف مسيريابي برنامه نياز است تا اين Url نيز به روز شود.
سه سطر بعدي نوع اطلاعاتي را كه بايد به سرور POST شوند مشخص مي‌كند. نوع json است و همچنين contentType آن براي ارسال اطلاعات يونيكد ضروري است. از متد JSON.stringify براي تبديل اشياء به رشته كمك گرفته‌ايم. اين متد در تمام مرورگرهاي امروزي به صورت توكار پشتيباني مي‌شود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحيحي encode شده و به سرور ارسال شوند. بنابراين اين رشته ارسالي اطلاعات را به صورت دستي تهيه نكنيد؛ چون كاراكترهاي زيادي هستند كه ممكن است مشكل ساز شده و بايد پيش از ارسال به سرور اصطلاحا escape يا encode شوند.
متداول است از پارامتر success براي دريافت نتيجه عمليات متد jQuery.Ajax استفاده شود. اما در اينجا از پارامتر complete آن استفاده شده است. علت هم اينجا است كه return PartialView يك رشته را بر مي‌گرداند. پارامتر success انتظار دريافت خروجي از نوع json را دارد. به همين جهت در اين مثال خاص بايد از پارامتر complete استفاده كرد تا بتوان به رشته بدون فرمت خروجي بدون مشكل دسترسي پيدا كرد.
به علاوه چون از يك section براي تعريف اسكريپت‌هاي مورد نياز استفاده كرده‌ايم، براي درج خودكار آن در هدر صفحه بايد قسمت هدر فايل layout برنامه را به صورت زير مقدار دهي كرد:

@RenderSection("javascript", required: false)



دسترسي به اطلاعات يك مدل در View، به كمك jQuery Ajax

اگر جزئي از صفحه كه قرار است به روز شود، پيچيده است، روش استفاده از partial viewها توصيه مي‌شود؛ براي مثال مي‌توان اطلاعات يك مدل را به همراه يك گريد كامل از اطلاعات، رندر كرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصيت از مدلي نياز داشتيم، مي‌توان از روش‌هايي با سربار كمتر نيز استفاده كرد. براي مثال متد جديد زير را به كنترلر Home اضافه كنيد:

[HttpPost] //for IE-8        
public ActionResult EmployeeInfoData(int? id)
{
            if (!Request.IsAjaxRequest())
                return Json(false);

            if (!id.HasValue)
                return Json(false);

            var list = EmployeeDataSource.CreateEmployees();
            var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
            if (data == null)
                return Json(false);

            return Json(data);
}

سپس View برنامه را نيز به نحو زير تغيير دهيد:

<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
    <script type="text/javascript">
        function LoadEmployeeInfoData() {
            showProgress();
            $.ajax({
                type: "POST",
                url: "/Home/EmployeeInfoData",
                data: JSON.stringify({ id: 1 }),
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                // controller is returning the json data
                success: function (result) {
                    if (result) {
                        alert(result.Id + ' - ' + result.Name);
                    }
                    hideProgress();
                },
                error: function (result) {
                    alert(result.status + ' ' + result.statusText);
                    hideProgress();
                }
            });
        }

        function showProgress() {
            $('#Progress').css("display", "block");
        }
        function hideProgress() {
            $('#Progress').css("display", "none");
        }
    </script>
}

در اين مثال، كنترلر برنامه، اطلاعات مدل را تبديل به Json كرده و بازگشت خواهد داد. سپس مي‌توان به اطلاعات اين مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق كدهاي فوق دسترسي يافت. اينبار چون خروجي كنترلر تعريف شده از نوع Json است، امكان استفاده از پارامتر success فراهم شده است. همه چيز هم در اينجا خودكار است؛ تبديل يك شيء به Json و برعكس.
يك نكته: اگر نوع متد كنترلر، HttpGet باشد، نياز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوي JsonRequestBehavior.AllowGet قرار داده شود.


ارسال اطلاعات فرم‌ها به سرور، به كمك ويژگي‌هاي Ajax

متد كمكي توكار ديگري به نام Ajax.BeginForm در ASP.NET MVC وجود دارد كه كار ارسال غيرهمزمان اطلاعات يك فرم را به سرور انجام داده و سپس اطلاعاتي را از سرور دريافت و قسمتي از صفحه را به روز خواهد كرد. مكانيزم كاري كلي آن بسيار شبيه به متد Ajax.ActionLink مي‌باشد. در ادامه با تكميل مثال قسمت جاري، به بررسي اين ويژگي خواهيم پرداخت.
ابتدا متد جستجوي زير را به كنترلر برنامه اضافه كنيد:

[HttpPost] //for IE-8        
public ActionResult SearchEmployeeInfo(string data)
{
            if (!Request.IsAjaxRequest())
                return Content(string.Empty);

            if (string.IsNullOrWhiteSpace(data))
                return Content(string.Empty);

            var employeesList = EmployeeDataSource.CreateEmployees();
            var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
            if (list == null || !list.Any())
                return Content(string.Empty);

            return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}

سپس بر روي نام متد كليك راست كرده و گزينه add view را انتخاب كنيد. در صفحه باز شده، گزينه create a stronlgly typed view را انتخاب كرده و قالب scaffolding را هم بر روي list قرار دهيد. سپس گزينه ايجاد partial view را نيز انتخاب كنيد. نام آن‌را هم _SearchEmployeeInfo وارد نمائيد. براي نمونه خروجي حاصل به نحو زير خواهد بود:

@model IEnumerable<MvcApplication18.Models.Employee>

<table>
    <tr>
        <th>
            Name
        </th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Name)
        </td>
    </tr>
}

</table>

تا اينجا يك متد جستجو را ايجاد كرده‌ايم كه مي‌تواند ليستي از ركوردهاي كارمندان را بر اساس قسمتي از نام آن‌ها كه توسط كاربري جستجو شده است، بازگشت دهد. سپس اين اطلاعات را به partial view مورد نظر ارسال كرده و يك جدول را بر اساس آن توليد خواهيم نمود.
اكنون به فايل Index.cshtml مراجعه كرده و فرم Ajax ايي زير را اضافه نمائيد:

@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
                       controllerName: "Home", 
                       ajaxOptions: new AjaxOptions
                                    {
                                         HttpMethod = "POST",
                                         InsertionMode = InsertionMode.Replace,
                                         UpdateTargetId = "EmployeeInfo",
                                         LoadingElementId = "Progress"
                                    }))
    {
      @Html.TextBox("data")
      <input type="submit" value="Search" />
    }

اينبار بجاي استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به كمك آن اطلاعات Html.TextBox تعريف شده، به كنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعريف شده، ارسال گرديده و نتيجه آن در يك div با id مساوي EmployeeInfo درج مي‌گردد. همچنين اگر اطلاعاتي يافت نشد، به كمك متد return Content يك رشته خالي بازگشت داده مي‌شود.
متد Ajax.BeginForm نيز از ويژگي‌هاي data-* براي تعريف اطلاعات مورد نياز ارسالي به سرور استفاده مي‌كند. بنابراين نياز به سطر الحاق jquery.unobtrusive-ajax.min.js در فايل layout برنامه جهت وفق دادن اين اطلاعات unobtrusive به اطلاعات مورد نياز متد jQuery.Ajax وجود دارد.

<form action="/Home/SearchEmployeeInfo" data-ajax="true" 
          data-ajax-loading="#Progress" data-ajax-method="POST" 
          data-ajax-mode="replace" data-ajax-update="#EmployeeInfo" 
          id="form0" method="post">
       <input id="data" name="data" type="text" value="" />
       <input type="submit" value="Search" />
</form>


كتابخانه كمكي «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهاي توكار Ajax همراه با ASP.NET MVC، ساير علاقمندان نيز يك سري Ajax helper را بر اساس افزونه‌هاي jQuery تدارك ديده‌اند كه از آدرس زير قابل دريافت هستند:
http://awesome.codeplex.com/


افزودن فرم‌ها به كمك jQuery.Ajax و فعال سازي اعتبار سنجي سمت كلاينت

در ASP.NET MVC چون ViewState حذف شده است، امكان تزريق فرم‌هاي جديد به صفحه يا به روز رساني قسمتي از صفحه توسط jQuery Ajax به سهولت و بدون دريافت پيغام «viewstate is corrupted» در حين ارسال اطلاعات به سرور، ميسر است.
در اين حالت بايد به يك نكته مهم نيز دقت داشت: «اعتبار سنجي سمت كلاينت ديگر كار نمي‌كند». علت اينجا است كه در حين بارگذاري متداول يك صفحه، متد زير به صورت خودكار فراخواني مي‌گردد:
$.validator.unobtrusive.parse("#{form-id}");

اما با به روز رساني قسمتي از صفحه، ديگر اينچنين نخواهد بود و نياز است اين فراخواني را دستي انجام دهيم. براي مثال:

$.ajax
({
    url: "/{controller}/{action}/{id}",
    type: "get",
    success: function(data)
    {
        $.validator.unobtrusive.parse("#{form-id}");
    }
});

//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });

شبيه به همين مساله را حين استفاده از Ajax.BeginForm نيز بايد مد نظر داشت:

@using (Ajax.BeginForm(
    "Action1",
    "Controller",
    null,
    new AjaxOptions { 
        OnSuccess = "onSuccess",
        UpdateTargetId = "result"
    },
    null)
)
{
    <input type="submit" value="Save" />
}

var onSuccess = function(result) {
    // enable unobtrusive validation for the contents
    // that was injected into the <div id="result"></div> node
    $.validator.unobtrusive.parse("#result");
};

در اين مثال در پارامتر UpdateTargetId، مجددا يك فرم رندر مي‌شود. بنابراين اعتبار سنجي سمت كلاينت آن ديگر كار نخواهد كرد مگر اينكه با مقدار دهي خاصيت OnSuccess، مجددا متد unobtrusive.parse را فراخواني كنيم.


۱۳۹۱/۰۲/۰۳

ASP.NET MVC #20


تهيه گزارشات تحت وب به كمك WebGrid

WebGrid از ASP.NET MVC 3.0 به صورت توكار به شكل يك Html Helper در دسترس مي‌باشد و هدف از آن ساده‌تر سازي تهيه گزارشات تحت وب است. البته اين گريد، تنها گريد مهياي مخصوص ASP.NET MVC نيست و پروژه MVC Contrib يا شركت Telerik نيز نمونه‌هاي ديگري را ارائه داده‌اند؛ اما از اين جهت كه اين Html Helper، بدون نياز به كتابخانه‌هاي جانبي در دسترس است، بررسي آن ضروري مي‌باشد.


صورت مساله

ليستي از كارمندان به همراه حقوق ماهيانه آن‌ها در دست است. اكنون نياز به گزارشي تحت وب، با مشخصات زير مي‌باشد:
1- گزارش بايد داراي صفحه بندي بوده و هر صفحه تنها 10 رديف را نمايش دهد.
2- سطرها بايد يك در ميان داراي رنگي متفاوت باشند.
3- ستون حقوق كارمندان در پايين هر صفحه، بايد داراي جمع باشد.
4- بتوان با كليك بر روي عنوان هر ستون، اطلاعات را بر اساس ستون انتخابي، مرتب ساخت.
5- لينك‌هاي حذف يا ويرايش يك رديف نيز در اين گزارش مهيا باشد.
6- ليست تهيه شده، داراي ستوني به نام «رديف» نيست. اين ستون را نيز به صورت خودكار اضافه كنيد.
7- ليست نهايي اطلاعات، داراي ستوني به نام ماليات نيست. فقط حقوق كارمندان ذكر شده است. ستون محاسبه شده ماليات نيز بايد به صورت خودكار در اين گزارش نمايش داده شود. اين ستون نيز بايد داراي جمع پايين هر صفحه باشد.
8- تمام اعداد اين گزارش در حين نمايش بايد داراي جدا كننده سه رقمي باشند.
9- تاريخ‌هاي موجود در ليست، ميلادي هستند. نياز است اين تاريخ‌ها در حين نمايش شمسي شوند.
10- انتهاي هر صفحه گزارش بايد بتوان برچسب «صفحه y/n» را مشاهده كرد. n در اينجا منظور تعداد كل صفحات است و y شماره صفحه جاري مي‌باشد.
11- انتهاي هر صفحه گزارش بايد بتوان برچسب «ركوردهاي y تا x از n» را مشاهده كرد. n در اينجا منظور تعداد كل ركوردها است.
12- نام كوچك هر كارمند، ضخيم نمايش داده شود.
13- به ازاي هر شماره كارمندي، يك تصوير در پوشه images سايت وجود دارد. براي مثال images/id.jpg. ستوني براي نمايش تصوير متناظر با هر كارمند نيز بايد اضافه شود.
14- به ازاي هر كارمند، تعدادي پروژه هم وجود دارد. پروژه‌هاي متناظر را توسط يك گريد تو در تو نمايش دهيد.


راه حل به كمك استفاده از WebGrid

ابتدا يك پروژه خالي ASP.NET MVC را آغاز كنيد. سپس مدل‌هاي زير را به آن اضافه نمائيد (يك كارمند كه مي‌تواند تعداد پروژه منتسب داشته باشد):

using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime AddDate { get; set; }
        public double Salary { get; set; }
        public IList<Project> Projects { get; set; }
    }
}

namespace MvcApplication17.Models
{
    public class Project
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }
}

سپس منبع داده نمونه زير را به پروژه اضافه كنيد. به عمد از ORM‌ خاصي استفاده نشده تا بتوانيد پروژه جاري را به سادگي در يك پروژه آزمايشي جديد،‌ تكرار كنيد.
using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
    public static class EmployeeDataSource
    {
        public static IList<Employee> CreateEmployees()
        {
            var list = new List<Employee>();
            var rnd = new Random();
            for (int i = 1; i <= 1000; i++)
            {
                list.Add(new Employee
                    {
                        Id = i + 1000,
                        FirstName = "fName " + i,
                        LastName = "lName " + i,
                        AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
                        Salary = rnd.Next(400, 3000),
                        Projects = CreateRandomProjects()
                    });
            }
            return list;
        }

        private static IList<Project> CreateRandomProjects()
        {
            var list = new List<Project>();
            var rnd = new Random();
            for (int i = 0; i < rnd.Next(1, 7); i++)
            {
                list.Add(new Project
                {
                    Id = i,
                    Name = "Project " + i
                });
            }
            return list;
        }
    }
}


در ادامه يك كنترلر جديد را با محتواي زير اضافه نمائيد:
using System.Web.Mvc;
using MvcApplication17.Models;

namespace MvcApplication17.Controllers
{
    public class HomeController : Controller
    {
        [HttpPost]
        public ActionResult Delete(int? id)
        {
            return RedirectToAction("Index");
        }

        [HttpGet]
        public ActionResult Edit(int? id)
        {
            return View();
        }

        [HttpGet]
        public ActionResult Index(string sort, string sortdir, int? page = 1)
        {
            var list = EmployeeDataSource.CreateEmployees();
            return View(list);
        }
    }
}

علت تعريف متد index با پارامترهاي sort و غيره به URLهاي خودكاري از نوع زير بر مي‌گردد:

http://localhost:3034/?sort=LastName&sortdir=ASC&page=3

همانطور كه ملاحظه مي‌كنيد، گريد رندر شده، از يك سري كوئري استرينگ براي مشخص سازي صفحه جاري، يا جهت مرتب سازي (صعودي و نزولي بودن آن) يا فيلد پيش فرض مرتب سازي، كمك مي‌گيرد.

سپس يك View خالي را نيز براي متد Index ايجاد كنيد. تا اينجا تنظيمات اوليه پروژه انجام شد.
كدهاي كامل View را در ادامه ملاحظه مي‌كنيد:

@using System.Globalization
@model IList<MvcApplication17.Models.Employee>
           
@{
    ViewBag.Title = "Index";
}

@helper WebGridPageFirstItem(WebGrid grid)
{
    @(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}

@helper WebGridPageLastItem(WebGrid grid)
{
    if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
    {
      @grid.TotalRowCount;
    }
    else
    {
      @((grid.PageIndex + 1) * grid.RowsPerPage);
    }
}

<h2>Employees List</h2>

@{ 
    var grid = new WebGrid(
        source: Model,
        canPage: true,
        rowsPerPage: 10,
        canSort: true,
        defaultSort: "FirstName"
    );
    var salaryPageSum = 0;
    var taxPageSum = 0;
    var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

<div id="container">
    @grid.GetHtml(
            tableStyle: "webgrid",
            headerStyle: "webgrid-header",
            footerStyle: "webgrid-footer",
            alternatingRowStyle: "webgrid-alternating-row",
            selectedRowStyle: "webgrid-selected-row",
            rowStyle: "webgrid-row-style",
            htmlAttributes: new { id = "MyGrid" },
            mode: WebGridPagerModes.All,
            columns: grid.Columns(
                 grid.Column(header: "#",
                             style: "text-align-center-col",
                             format: @<text>@(rowIndex++)</text>),
                 grid.Column(columnName: "FirstName", header: "First Name",
                             format: @<span style='font-weight: bold'>@item.FirstName</span>,
                             style: "text-align-center-col"),
                 grid.Column(columnName: "LastName", header: "Last Name"),
                 grid.Column(header: "Image",
                             style: "text-align-center-col",
                             format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
                 grid.Column(columnName: "AddDate", header: "Start",
                             style: "text-align-center-col",
                             format: item =>
                             {
                                 int ym = item.AddDate.Year;
                                 int mm = item.AddDate.Month;
                                 int dm = item.AddDate.Day;
                                 var persianCalendar = new PersianCalendar();
                                 int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
                                 int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
                                 int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
                                 return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");

                             }),
                 grid.Column(columnName: "Salary", header: "Salary",
                             format: item =>
                             {
                                 salaryPageSum += item.Salary;
                                 return string.Format("${0:n0}", item.Salary);
                             },
                             style: "text-align-center-col"),
                 grid.Column(header: "Tax", canSort: true,
                             format: item =>
                             {
                                 var tax = item.Salary * 0.2;
                                 taxPageSum += tax;
                                 return string.Format("${0:n0}", tax);
                             }),
                 grid.Column(header: "Projects", columnName: "Projects",
                             style: "text-align-center-col",
                             format: item =>
                             {
                                 var subGrid = new WebGrid(
                                                        source: item.Projects,
                                                        canPage: false,
                                                        canSort: false
                                                        );
                                 return subGrid.GetHtml(
                                                        htmlAttributes: new { id = "MySubGrid" },
                                                        tableStyle: "webgrid",
                                                        headerStyle: "webgrid-header",
                                                        footerStyle: "webgrid-footer",
                                                        alternatingRowStyle: "webgrid-alternating-row",
                                                        selectedRowStyle: "webgrid-selected-row",
                                                        rowStyle: "webgrid-row-style"
                                     );
                             }),
                 grid.Column(header: "",
                             style: "text-align-center-col",
                             format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
                                                              controllerName: "Home", routeValues: new { id = item.Id },
                                                              htmlAttributes: null)),
                 grid.Column(header: "",
                             format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
                                            onclick="return confirm('Do you want to delete this record?');"
                                            value="Delete"/></form>),
                 grid.Column(header: "", format: item => item.GetSelectLink("Select"))
                      )
                  )

    <strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount, 
    <strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

    @*
    @if (@grid.HasSelection)
    {
        @RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
    }
    *@
</div>

@section script{
<script type="text/javascript">
    $(function () {
        $('#MyGrid tbody:first').append(
        '<tr class="total-row"><td></td>\
         <td></td><td></td><td></td>\
         <td><strong>Total:</strong></td>\
         <td>@string.Format("${0:n0}", @salaryPageSum)</td>\
         <td>@string.Format("${0:n0}", @taxPageSum)</td>\
         <td></td><td></td><td></td></tr>');
    });
</script>
}


توضيحات ريز جزئيات View فوق


تعريف ابتدايي شيء WebGrid و مقدار دهي آن
در ابتدا نياز است يك وهله از شيء WebGrid را ايجاد كنيم. در اينجا مي‌توان تنظيم كرد كه آيا نياز است اطلاعات نمايش داده شده داراي صفحه بندي (canPage) خودكار باشند؟ منبع داده (source) كدام است. در صورت فعال سازي صفحه بندي خودكار، چه تعداد رديف (rowsPerPage) در هر صفحه نمايش داده شود. آيا نياز است بتوان با كليك بر روي سر ستون‌ها، اطلاعات را بر اساس فيلد متناظر با آن مرتب (canSort) ساخت؟ همچنين در صورت نياز به مرتب سازي، اولين باري كه گريد نمايش داده مي‌شود، بر اساس چه فيلدي (defaultSort) بايد مرتب شده نمايش داده شود:

@{ 
    var grid = new WebGrid(
        source: Model,
        canPage: true,
        rowsPerPage: 10,
        canSort: true,
        defaultSort: "FirstName"
    );
    var salaryPageSum = 0;
    var taxPageSum = 0;
    var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

در اينجا همچنين سه متغير كمكي هم تعريف شده كه از اين‌ها براي تهيه جمع ستون‌هاي حقوق و ماليات و همچنين نمايش شماره رديف جاري استفاده مي‌شود. فرمول نحوه محاسبه اولين رديف هر صفحه را هم ملاحظه مي‌كنيد. شماره رديف‌هاي بعدي، rowIndex++ خواهند بود.


تعريف رنگ و لعاب گريد نمايش داده شده
در ادامه به كمك متد grid.GetHtml، رشته‌اي معادل اطلاعات HTML صفحه جاري، بازگشت داده مي‌شود. در اينجا مي‌توان يك سري خواص تكميلي را تنظيم نمود. براي مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },

هر كدام از اين رشته‌ها در حين رندر نهايي گريد،‌ تبديل به يك class خواهند شد. براي نمونه:

<div id="container">
      <table class="webgrid" id="MyGrid">
        <thead>
          <tr class="webgrid-header">

به اين ترتيب با اندكي ويرايش css سايت، مي‌توان انواع و اقسام رنگ‌ها را به سطرها و ستون‌هاي گريد نهايي اعمال كرد. براي مثال اطلاعات زير را به فايل css سايت اضافه نمائيد:

/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
 width: 100%;
 margin: 0px;
 padding: 0px;
 border: 0px;
 border-collapse: collapse;
 font-family: Tahoma;
 font-size: 9pt;
}

.webgrid a
{
 color: #000;
}

.webgrid-header
{
 padding: 0px 5px;
 text-align: center;
 border-bottom: 2px solid #739ace;
 height: 20px;
 border-top: 2px solid #D6E8FF;
 border-left: 2px solid #D6E8FF;
 border-right: 2px solid #D6E8FF;
}

.webgrid-header th
{
 background-color: #eaf0ff;
 border-right: 1px solid #ddd;
}

.webgrid-footer
{
 padding: 6px 5px;
 text-align: center;
 background-color: #e8eef4;
 border-top: 2px solid #3966A2;
 height: 25px;
 border-bottom: 2px solid #D6E8FF;
 border-left: 2px solid #D6E8FF;
 border-right: 2px solid #D6E8FF;
}

.webgrid-alternating-row
{
 height: 22px;
 background-color: #f2f2f2;
 border-bottom: 1px solid #d2d2d2;
 border-left: 2px solid #D6E8FF;
 border-right: 2px solid #D6E8FF;
}

.webgrid-row-style
{
 height: 22px;
 border-bottom: 1px solid #d2d2d2;
 border-left: 2px solid #D6E8FF;
 border-right: 2px solid #D6E8FF;
}

.webgrid-selected-row
{
 font-weight: bold;
}

.text-align-center-col
{
 text-align: center;
}

.total-row
{
 background-color:#f9eef4;
}

همانطور كه ملاحظه مي‌كنيد، رنگ‌هاي رديف‌ها، هدر و فوتر گريد و غيره در اينجا تنظيم مي‌شوند.
به علاوه اگر دقت كرده باشيد در تعاريف گريد، htmlAttributes هم مقدار دهي شده است. در اينجا به كمك يك anonymously typed object، مقدار id گريد مشخص شده است. از اين id در حين كار با jQuery‌ استفاده خواهيم كرد.


تعيين نوع Pager
پارامتر ديگري كه در متد grid.GetHtml تنظيم شده است، mode: WebGridPagerModes.All مي‌باشد. WebGridPagerModes يك enum با محتواي زير است و توسط آن مي‌توان نوع Pager گريد را تعيين كرد:

[Flags]
public enum WebGridPagerModes
{
        Numeric = 1,
        //
        NextPrevious = 2,
        //
        FirstLast = 4,
        //
        All = 7,
}

نحوه تعريف ستون‌هاي گريد
اكنون به مهم‌ترين قسمت تهيه گزارش رسيده‌ايم. در اينجا با مقدار دهي پارامتر columns، نحوه نمايش اطلاعات ستون‌هاي مختلف مشخص مي‌گردد. مقداري كه بايد در اينجا تنظيم شود، آرايه‌اي از نوع WebGridColumn مي‌باشد و مرسوم است به كمك متد كمكي grid.Columns،‌ اينكار را انجام داد.
متد كمكي grid.Column، يك وهله از شيء WebGridColumn را بر مي‌گرداند و از آن براي تعريف هر ستون استفاده خواهيم كرد. توسط پارامتر columnName آن،‌ نام فيلدي كه بايد اطلاعات ستون جاري از آن اخذ شود مشخص مي‌شود. به كمك پارامتر header،‌ عبارت سرستون متناظر تنظيم مي‌گردد. پارامتر format، مهم‌ترين و توانمندترين پارامتر متد grid.Column است:

grid.Column(columnName: "FirstName", header: "First Name",
                             format: @<span style='font-weight: bold'>@item.FirstName</span>,
                             style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),

پارامتر format، به نحو زير تعريف شده است:

Func<dynamic, object> format

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

format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
                     {
                          salaryPageSum += item.Salary;
                           return string.Format("${0:n0}", item.Salary);
                      }

مستقيما از توانمندي‌هاي Razor استفاده كنيد. مثلا يك تگ كامل را بدون نياز به محصور سازي آن بين "" شروع كنيد. سپس @item به وهله‌اي از ركورد در دسترس اشاره مي‌كند كه در اينجا وهله‌اي از شيء كارمند است.
و يا همانند روشي كه براي محاسبه جمع حقوق هر صفحه مشاهده مي‌كنيد، مستقيما از lambda expressions براي تعريف يك anonymous delegate استفاده كنيد.


نحوه اضافه كردن ستون رديف
ستون رديف، يك ستون محاسبه شده (calculated field) است:

grid.Column(header: "#",
                 style: "text-align-center-col",
                 format: @<text>@(rowIndex++)</text>),

نيازي نيست حتما يك grid.Column، به فيلدي در كلاس كارمند اشاره كند. مقدار سفارشي آن را به كمك پارامتر format تعيين خواهيم كرد. هر بار كه قرار است يك رديف رندر شود، يكبار اين پارامتر فراخواني خواهد شد. فرمول محاسبه rowIndex ابتداي صفحه را نيز پيشتر ملاحظه نموديد.


نحوه اضافه كردن ستون سفارشي تصاوير كارمندها
ستون تصوير كارمندها نيز مستقيما در كلاس كارمند تعريف نشده است. بنابراين مي‌توان آن‌را با مقدار دهي صحيح پارامتر format ايجاد كرد:

grid.Column(header: "Image",
                  style: "text-align-center-col",
                  format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),


در اين مثال، تصاوير كارمندها در پوشه images واقع در ريشه سايت، قرار دارند. به همين جهت از متد Url.Content براي مقدار دهي صحيح آن استفاده كرديم. به علاوه در اينجا @item.Id به Id ركورد در حال رندر اشاره مي‌كند.


نحوه تبديل تاريخ‌ها به تاريخ شمسي
در ادامه بازهم به كمك پارامتر format، يك وهله از شيء dynamic اشاره كننده به ركورد در حال رندر را دريافت مي‌كنيم. سپس فرصت خواهيم داشت تا بر اين اساس، فرمول نويسي كنيم. دست آخر هم رشته مورد نظر نهايي را بازگشت مي‌دهيم:

grid.Column(columnName: "AddDate", header: "Start",
                  style: "text-align-center-col",
                  format: item =>
                  {
                       int ym = item.AddDate.Year;
                       int mm = item.AddDate.Month;
                       int dm = item.AddDate.Day;
                       var persianCalendar = new PersianCalendar();
                       int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
                       int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
                       int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
                       return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
                    }),


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

grid.Column(columnName: "Salary", header: "Salary",
                 format: item =>
                             {
                                 salaryPageSum += item.Salary;
                                 return string.Format("${0:n0}", item.Salary);
                             },
                             style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
                             format: item =>
                             {
                                 var tax = item.Salary * 0.2;
                                 taxPageSum += tax;
                                 return string.Format("${0:n0}", tax);
                             }),


اضافه كردن گرديد‌هاي تو در تو
متد Grid.GetHtml، يك رشته را بر مي‌گرداند. بنابراين در هر چند سطح كه نياز باشد مي‌توان يك گريد را بر اساس اطلاعات دردسترس رندر كرد و سپس بازگشت داد:

grid.Column(header: "Projects", columnName: "Projects",
                  style: "text-align-center-col",
                  format: item =>
                  {
                                 var subGrid = new WebGrid(
                                                        source: item.Projects,
                                                        canPage: false,
                                                        canSort: false
                                                        );
                                 return subGrid.GetHtml(
                                                        htmlAttributes: new { id = "MySubGrid" },
                                                        tableStyle: "webgrid",
                                                        headerStyle: "webgrid-header",
                                                        footerStyle: "webgrid-footer",
                                                        alternatingRowStyle: "webgrid-alternating-row",
                                                        selectedRowStyle: "webgrid-selected-row",
                                                        rowStyle: "webgrid-row-style"
                                     );
                       }),


در اينجا كار اصلي از طريق پارامتر format شروع مي‌شود. سپس به كمك item.Projects به ليست پروژه‌هاي هر كارمند دسترسي خواهيم داشت. بر اين اساس يك گريد جديد را توليد كرد و سپس رشته معادل با آن را به كمك متد subGrid.GetHtml دريافت و بازگشت مي‌دهيم. اين رشته در سلول جاري درج خواهد شد. به نوعي يك گزارش master detail يا sub report را توليد كرده‌ايم.


اضافه كردن دكمه‌هاي ويرايش، حذف و انتخاب
هر سه دكمه ويرايش، حذف و انتخاب در ستون‌هايي سفارشي قرار خواهند گرفت. بنابراين مقدار دهي header و format متد grid.Column كفايت مي‌كند:

grid.Column(header: "",
                  style: "text-align-center-col",
                   format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
                                                              controllerName: "Home", routeValues: new { id = item.Id },
                                                              htmlAttributes: null)),
grid.Column(header: "",
                  format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
                                            onclick="return confirm('Do you want to delete this record?');"
                                            value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))


نكته جديدي كه در اينجا وجود دارد متد item.GetSelectLink مي‌باشد. اين متد جزو متدهاي توكار گريد است و كار آن بازگشت دادن شيء grid.SelectedRow مي‌باشد. اين شيء پويا، حاوي اطلاعات ركورد انتخاب شده است. براي مثال اگر نياز باشد اين اطلاعات به صفحه‌اي ارسال شود، مي‌توان از روش زير استفاده كرد:

@if (@grid.HasSelection)
    {
        @RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
    }


نمايش برچسب‌هاي صفحه x از n و ركوردهاي x تا y از z
در يك گزارش خوب بايد مشخص باشد كه صفحه جاري، كدامين صفحه از چه تعداد صفحه كلي است. يا ركوردهاي صفحه جاري چه بازه‌اي از تعداد ركوردهاي كلي را تشكيل مي‌دهند. براي اين منظور چند متد كمكي به نام‌هاي WebGridPageFirstItem و WebGridPageLastItem تهيه شده‌اند كه آن‌ها را در ابتداي View ارائه شده، مشاهده نموديد:

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount, 
    <strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

نمايش جمع ستون‌هاي حقوق و ماليات در هر صفحه
گريد توكار همراه با ASP.NET MVC در اين مورد راه حلي را ارائه نمي‌دهد. بنابراين بايد اندكي دست به ابتكار زد. مثلا:

@section script{
<script type="text/javascript">
    $(function () {
        $('#MyGrid tbody:first').append(
        '<tr class="total-row"><td></td>\
         <td></td><td></td><td></td>\
         <td><strong>Total:</strong></td>\
         <td>@string.Format("${0:n0}", @salaryPageSum)</td>\
         <td>@string.Format("${0:n0}", @taxPageSum)</td>\
         <td></td><td></td><td></td></tr>');
    });
</script>
}

در اين مثال به كمك jQuery با توجه به اينكه id گريد ما MyGrid است، يك رديف سفارشي كه همان جمع محاسبه شده است، به tbody جدول نهايي توليدي اضافه مي‌شود. از tbody:first هم در اينجا استفاده شده است تا رديف اضافه شده به گريدهاي تو در تو اعمال نشود.
سپس فايل Views\Shared\_Layout.cshtml را گشوده و از section تعريف شده، براي مقدار دهي master page سايت، استفاده نمائيد:

<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>
    @RenderSection("script", required: false)
</head>

۱۳۹۱/۰۲/۰۱

ASP.NET MVC #19


مروري بر امكانات Caching اطلاعات در ASP.NET MVC

در برنامه‌هاي وب، بالاترين حد كارآيي برنامه‌ها از طريق بهينه سازي الگوريتم‌ها حاصل نمي‌شود، بلكه با بكارگيري امكانات Caching سبب خواهيم شد تا اصلا كدي اجرا نشود. در ASP.NET MVC اين هدف از طريق بكارگيري فيلتري به نام OutputCache ميسر مي‌گردد:

using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
    public class HomeController : Controller
    {
        [OutputCache(Duration = 60, VaryByParam = "none")]
        public ActionResult Index()
        {
            return View();
        }
    }
}

همانطور كه ملاحظه مي‌كنيد، OutputCache را به يك اكشن متد يا حتي به يك كنترلر نيز مي‌توان اعمال كرد. به اين ترتيب HTML نهايي حاصل از View متناظر با اكشن متد جاري فراخواني شده، Cache خواهد شد. سپس زمانيكه درخواست بعدي به سرور ارسال مي‌شود، نتيجه دريافت شده، همان اطلاعات Cache شده قبلي است و عملا در سمت سرور كدي اجرا نخواهد شد. در اينجا توسط پارامتر Duration، مدت زمان معتبر بودن كش حاصل، برحسب ثانيه مشخص مي‌شود. VaryByParam مشخص مي‌كند كه اگر متدي پارامتري را دريافت مي‌كند، آيا بايد به ازاي هر مقدار دريافتي، مقادير كش شده متفاوتي ذخيره شوند يا خير. در اينجا چون متد Index پارامتري ندارد، از مقدار none استفاده شده است.


مثال يك
يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس كنترلر جديد Home را نيز به آن اضافه نمائيد:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
    public class HomeController : Controller
    {
        [OutputCache(Duration = 60, VaryByParam = "none")]
        public ActionResult Index()
        {
            ViewBag.ControllerTime = DateTime.Now;
            return View();
        }
    }
}

همچنين كدهاي View متد Index را نيز به نحو زير تغيير دهيد:

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

در اينجا نمايش دو زمان دريافتي از كنترلر و زمان محاسبه شده در View را مشاهده مي‌كنيد. هدف اين است كه بررسي كنيم آيا فيلتر OutputCache بر روي اين دو مقدار تاثيري دارد يا خير.
برنامه را اجرا نمائيد. سپس چند بار صفحه را Refresh كنيد. مشاهده خواهيد كرد كه هر دو زمان ياد شده تا 60 ثانيه، تغييري نخواهند كرد و حاصل نهايي از Cache خواهنده مي‌شود.
كاربرد يك چنين حالتي براي مثال نمايش اطلاعات بازديدهاي يك سايت است. نبايد به ازاي هر كاربر وارد شده به سايت، يكبار به بانك اطلاعاتي مراجعه كرد و آمار جديدي را تهيه نمود. يا براي نمونه اگر جايي قرار است اطلاعات وضعيت آب و هوا نمايش داده شود، بهتر است اين اطلاعات، مثلا هر نيم ساعت يكبار به روز شود و نه به ازاي هر بازديد جديد از سايت، توسط صدها بازديد كننده همزمان. يا براي مثال كش كردن خروجي فيد RSS يك بلاگ به مدت چند ساعت نيز ايده خوبي است. از اين لحاظ كه اگر اطلاعات بلاگ شما روزي يكبار به روز مي‌شود، نيازي نيست تا به ازاي هر برنامه فيدخوان، يكبار اطلاعات از بانك اطلاعاتي دريافت شده و پروسه رندر نهايي فيد صورت گيرد. منوهاي پوياي يك سايت نيز در همين رده قرار مي‌گيرند. دريافت اطلاعات منوهاي پوياي سايت به ازاي هر درخواست رسيده كاربري جديد، كار اشتباهي است. اين اطلاعات نيز بايد كش شوند تا بار سرور كاهش يابد. البته تمام اين‌ها زماني ميسر خواهند شد كه اطلاعات سمت سرور كش شوند.


مثال دو
همان مثال قبلي را در اينجا جهت بررسي پارامتر VaryByParam به نحو زير تغيير مي‌دهيم:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
    public class HomeController : Controller
    {
        [OutputCache(Duration = 60, VaryByParam = "none")]
        public ActionResult Index(string parameter)
        {
            ViewBag.Msg = parameter ?? string.Empty;
            ViewBag.ControllerTime = DateTime.Now;
            return View();
        }
    }
}


در اينجا يك پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بين تگ‌هاي h2 نمايش داده خواهد شد. همچنين يك فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:

@{
    ViewBag.Title = "Index";
}

<h2>@ViewBag.Msg</h2>

<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

@using (Html.BeginForm())
{
    @Html.TextBox("parameter")
    <input type="submit" />
}

اكنون برنامه را اجرا كنيد. در TextBox نمايش داده شده يكبار مثلا بنويسيد Test1 و فرم را به سرور ارسال نمائيد. سپس مقدار Test2 را وارد كرده و ارسال نمائيد. در بار دوم، خروجي صفحه همانند زماني است كه مقدار Test1 ارسال شده است. علت اين است كه مقدار VaryByParam به none تنظيم شده است و صرفنظر از ورودي كاربر، همان اطلاعات كش شده قبلي بازگشت داده خواهد شد. براي رفع اين مشكل، متد Index را به نحو زير تغيير دهيد، به طوريكه مقدار VaryByParam به نام پارامتر متد جاري اشاره كند:

[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)

در ادامه مجددا برنامه را اجرا كنيد. اكنون يكبار مقدار Test1 را به سرور ارسال كنيد. سپس مقدار Test2 را ارسال نمائيد. مجددا همين دو مرحله را با مقادير Test1 و Test2 تكرار كنيد. مشاهده خواهيد كرد كه اينبار اطلاعات بر اساس مقدار پارامتر ارسالي كش شده است.



تنظيمات متفاوت OutputCache

الف) VaryByParam : اگر مساوي none قرار گيرد، همواره همان مقدار كش شده قبلي نمايش داده مي‌شود. اگر مقدار آن به نام پارامتر خاصي تنظيم شود، اطلاعات كش شده بر اساس مقادير متفاوت پارامتر دريافتي، متفاوت خواهند بود. در اينجا پارامترهاي متفاوت را با يك «,» مي‌توان از هم جدا ساخت. اگر تعداد پارامترها زياد است مي‌توان مقدار VaryByParam را مساوي با * قرار داد. در اين حالت به ازاي مقادير متفاوت دريافتي پارامترهاي مختلف، اطلاعات مجزايي در كش قرار خواهد گرفت. اين روش آخر آنچنان توصيه نمي‌شود چون سربار بالايي دارد و حجم بالايي از اطلاعات بر اساس پارامترهاي مختلف، بايد در كش قرار گيرند.
ب) Location : مكان قرارگيري اطلاعات كش شده را مشخص مي‌كند. مقدار آن نيز بر اساس يك enum به نام OutputCacheLocation مشخص مي‌گردد. در اين حالت براي مثال مي‌توان مكان‌هاي Server، Client و ServerAndClient را مقدار دهي نمود. مقدار Downstream به معناي كش شدن اطلاعات بر روي پروكسي سرورهاي بين راه و يا مرورگرها است. پيش فرض آن Any است كه تركيبي از Server و Downstream مي‌باشد.
اگر قرار است اطلاعات يكساني به تمام كاربران نمايش داده شود، مثلا محتواي ليست يك منوي پويا،‌ محل قرارگيري اطلاعات كش بايد سمت سرور باشد. اگر نياز است به ازاي هر كاربر محتواي اطلاعات كش شده متفاوت باشد، بهتر است محل سمت كلاينت را مقدار دهي نمود.
ج) VaryByHeader : اطلاعات، بر اساس هدرهاي مشخص شده، كش مي‌شوند. براي مثال مرسوم است كه از Accept-Language در اينجا استفاده شود تا اطلاعات مثلا فرانسوي كش شده، به كاربر آلماني تحويل داده نشود.
د) VaryByCustom :‌ در اين حالت نام يك متد استاتيك تعريف شده در فايل global.asax.cs بايد مشخص گردد. توسط اين متد كليد رشته‌اي اطلاعاتي كه قرار است كش شود، بازگشت داده خواهد شد.
ه) SqlDependency : در اين حالت اطلاعات تا زمانيكه تغييري در جداول بانك اطلاعاتي SQL Server صورت نگيرد، كش خواهد شد.
و) Nostore : به پروكسي سرورهاي بين راه و همچنين مرورگرها اطلاع مي‌دهد كه اطلاعات را نبايد كش كنند. اگر قسمت اعتبار سنجي اين سري را به خاطر داشته باشيد، چنين تعريفي در قسمت Remote validation بكارگرفته شد:

[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]  

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

using System;
using System.Web.Mvc;

namespace MvcApplication16.Helper
{
    /// <summary>
    /// Adds "Cache-Control: private, max-age=0" header,
    /// ensuring that the responses are not cached by the user's browser. 
    /// </summary>
    public class NoCachingAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            base.OnActionExecuted(filterContext);
            filterContext.HttpContext.Response.CacheControl = "private";
            filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
        }
    }
}

كار اين فيلتر اضافه كردن هدر «Cache-Control: private, max-age=0» به Response است.


استفاده از فايل Web.Config براي معرفي تنظيمات Caching

يكي ديگر از تنظيمات ويژگي OutputCache، پارامتر CacheProfile است كه امكان تنظيم آن در فايل web.config نيز وجود دارد. براي نمونه تنظيمات زير را به قسمت system.web فايل وب كانفيگ برنامه اضافه كنيد:


<system.web>
    <caching>
      <outputCacheSettings>
        <outputCacheProfiles>
          <add name="Aggressive" location="ServerAndClient" duration="300"/>
          <add name="Mild" duration="100"  location="Server" />
        </outputCacheProfiles>
      </outputCacheSettings>
    </caching>

سپس مثلا براي استفاده از پروفايلي به نام Aggressive، خواهيم داشت:

[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)


استفاده از ويژگي به نام donut caching

تا اينجا به اين نتيجه رسيديم كه OutputCache، كل خروجي يك View را بر اساس پارامترهاي مختلفي كه دريافت مي‌كند، كش خواهد كرد. در اين بين اگر بخواهيم تنها قسمت كوچكي از صفحه كش نشود چه بايد كرد؟ براي حل اين مشكل قابليتي به نام cache substitution كه به donut caching هم معروف است (چون آن‌را مي‌توان به شكل يك donut تصور كرد!) در ASP.NET MVC قابل استفاده است.

@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }

همانطور كه ملاحظه مي‌كنيد براي تعريف يك چنين اطلاعاتي بايد از متد Response.WriteSubstitution در يك view استفاده كرد. در اين مثال، نمايش زمان جاري معرفي شده، صرف نظر از وضعيت كش صفحه جاري، كش نخواهد شد.

عكس آن هم ممكن است. فرض كنيد كه صفحه جاري شما از سه partial view تشكيل شده است. هر كدام از اين partial viewها نيز مزين به OutpuCache هستند. اما صفحه اصلي درج كننده اطلاعات اين سه partial view فاقد ويژگي Output كش است. در اين حالت تنها اطلاعات اين partial viewها كش خواهند شد و ساير قسمت‌هاي صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جديد به روز خواهند شد. حالت توصيه شده نيز همين مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومي درنظر داشته باشيد.


استفاده از امكانات Data Caching به صورت مستقيم

مطالبي كه تا اينجا عنوان شدند به كش كردن اطلاعات Response اختصاص داشتند. اما امكانات Caching موجود، به اين مورد خلاصه نشده و مي‌توان اطلاعات و اشياء را نيز كش كرد. براي مثال اطلاعات «با سطح دسترسي عمومي» دريافتي از بانك اطلاعاتي توسط يك كوئري را نيز مي‌توان كش كرد. جهت انجام اينكار مي‌توان از متدهاي HttpRuntime.Cache.Insert و يا HttpContext.Cache.Insert استفاده كرد. استفاده از HttpContext.Cache.Insert حين نوشتن Unit tests دردسر كمتري دارد و mocking آن ساده است؛ از اين جهت كه بر اساس HttpContextBase تعريف شده‌است.
در ادامه يك كلاس كمكي نوشتن اطلاعات در cache و سپس بازيابي آن‌را ملاحظه مي‌كنيد:

using System;
using System.Web;
using System.Web.Caching;

namespace MvcApplication16.Helper
{
    public static class CacheManager
    {
        public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
        {
            if (data == null) return;
            httpContext.Cache.Add(
                key,
                data,
                null,
                DateTime.Now.AddMinutes(durationMinutes),
                TimeSpan.Zero,
                CacheItemPriority.AboveNormal,
                null);
        }

        public static T CacheRead<T>(this HttpContextBase httpContext, string key)
        {
            var data = httpContext.Cache[key];
            if (data != null)
                return (T)data;
            return default(T);
        }

        public static void InvalidateCache(this HttpContextBase httpContext, string key)
        {
            httpContext.Cache.Remove(key);
        }
    }
}

و براي استفاده از آن در يك اكشن متد، ابتدا نياز است فضاي نام اين كلاس تعريف شود و سپس براي نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext يكي از خواص تعريف شده در شيء كنترلر است كه با ارث بري كنترلرها از آن، همواره در دسترس مي‌باشد.
در اينجا براي نمونه اطلاعات يك ليست جنريك دريافتي از بانك اطلاعاتي را مثلا 10 دقيقه (بسته به پارامتر durationMinutes آن) مي‌توان كش كرد و سپس توسط متد CacheRead آن‌را دريافت نمود. اگر متد CacheRead نال برگرداند به معناي خالي بودن كش است. بنابراين يكبار اطلاعات را از بانك اطلاعاتي دريافت نموده و سپس آن‌را كش خواهيم كرديم.
البته هستند ORMهايي كه يك چنين كارهايي را به صورت توكار پشتيباني كنند. به مكانيزم آن، Second level cache هم گفته مي‌شود؛ به علاوه امكان استفاده از پروايدرهاي ديگري را بجز كش IIS براي ذخيره سازي موقتي اطلاعات نيز فراهم مي‌كنند.
همچنين بايد دقت داشت اين اعداد مدت زمان، هيچگونه ضمانتي ندارند. اگر IIS احساس كند كه با كمبود منابع مواجه شده است، به سادگي شروع به حذف اطلاعات موجود در كش خواهد كرد.


نكته امنيتي مهم!
به هيچ عنوان از OutputCache در صفحاتي كه نياز به اعتبار سنجي دارند، استفاده نكنيد و به همين جهت در قسمت كش كردن اطلاعات، بر روي «اطلاعاتي با سطح دسترسي عمومي» تاكيد شد.
فرض كنيد كارمندي به صفحه مشاهده فيش حقوقي خودش مراجعه كرده است. اين ماه هم اضافه حقوق آنچناني داشته است. شما هم اين صفحه را به مدت سه ساعت كش كرده‌ايد. آيا مي‌توانيد تصور كنيد اگر همين گزارش كش شده با اين اطلاعات، به ساير كارمندان نمايش داده شود چه قشقرقي به پا خواهد شد؟!
بنابراين هيچگاه اطلاعات مخصوص به يك كاربر اعتبار سنجي شده را كش نكنيد و «تنها» اطلاعاتي نياز به كش شدن دارند كه عمومي باشند. براي مثال ليست آخرين اخبار سايت؛ ليست آخرين مدخل‌هاي فيد RSS سايت؛ ليست اطلاعات منوي عمومي سايت؛ ليست تعداد كاربران مراجعه كننده به سايت در طول يك روز؛ گزارش آب و هوا و كليه اطلاعاتي با سطح دسترسي عمومي كه كش شدن آن‌ها مشكل ساز نباشد.
به صورت خلاصه هيچگاه در كدهاي شما چنين تعريفي نبايد مشاهده شود:
[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()




۱۳۹۱/۰۱/۳۱

ASP.NET MVC #18


اعتبار سنجي كاربران در ASP.NET MVC

دو مكانيزم اعتبارسنجي كاربران به صورت توكار در ASP.NET MVC در دسترس هستند: Forms authentication و Windows authentication.
در حالت Forms authentication، برنامه موظف به نمايش فرم لاگين به كاربر‌ها و سپس بررسي اطلاعات وارده توسط آن‌ها است. برخلاف آن، Windows authentication حالت يكپارچه با اعتبار سنجي ويندوز است. براي مثال زمانيكه كاربري به يك دومين ويندوزي وارد مي‌شود، از همان اطلاعات ورود او به شبكه داخلي، به صورت خودكار و يكپارچه جهت استفاده از برنامه كمك گرفته خواهد شد و بيشترين كاربرد آن در برنامه‌هاي نوشته شده براي اينترانت‌هاي داخلي شركت‌ها است. به اين ترتيب كاربران يك بار به دومين وارد شده و سپس براي استفاده از برنامه‌هاي مختلف ASP.NET، نيازي به ارائه نام كاربري و كلمه عبور نخواهند داشت. Forms authentication بيشتر براي برنامه‌هايي كه از طريق اينترنت به صورت عمومي و از طريق انواع و اقسام سيستم عامل‌ها قابل دسترسي هستند، توصيه مي‌شود (و البته منعي هم براي استفاده در حالت اينترانت ندارد).
ضمنا بايد به معناي اين دو كلمه هم دقت داشت: هدف از Authentication اين است كه مشخص گردد هم اكنون چه كاربري به سايت وارد شده است. Authorization، سطح دسترسي كاربر وارد شده به سيستم و اعمالي را كه مجاز است انجام دهد، مشخص مي‌كند.


فيلتر Authorize در ASP.NET MVC

يكي ديگر از فيلترهاي امنيتي ASP.NET MVC به نام Authorize، كار محدود ساختن دسترسي به متدهاي كنترلرها را انجام مي‌دهد. زمانيكه اكشن متدي به اين فيلتر يا ويژگي مزين مي‌شود، به اين معنا است كه كاربران اعتبارسنجي نشده، امكان دسترسي به آن‌را نخواهند داشت. فيلتر Authorize همواره قبل از تمامي فيلترهاي تعريف شده ديگر اجرا مي‌شود.
فيلتر Authorize با پياده سازي اينترفيس System.Web.Mvc.IAuthorizationFilter توسط كلاس System.Web.Mvc.AuthorizeAttribute در دسترس مي‌باشد. اين كلاس علاوه بر پياده سازي اينترفيس ياد شده، داراي دو خاصيت مهم زير نيز مي‌باشد:

public string Roles { get; set; } // comma-separated list of role names
public string Users { get; set; } // comma-separated list of usernames

زمانيكه فيلتر Authorize به تنهايي بكارگرفته مي‌شود، هر كاربر اعتبار سنجي شده‌اي در سيستم قادر خواهد بود به اكشن متد مورد نظر دسترسي پيدا كند. اما اگر همانند مثال زير، از خواص Roles و يا Users نيز استفاده گردد، تنها كاربران اعتبار سنجي شده مشخصي قادر به دسترسي به يك كنترلر يا متدي در آن خواهند شد:

[Authorize(Roles="Admins")]
public class AdminController : Controller
{
    [Authorize(Users="Vahid")]
    public ActionResult DoSomethingSecure()
    {
    }
}

در اين مثال، تنها كاربراني با نقش Admins قادر به دسترسي به كنترلر جاري Admin خواهند بود. همچنين در بين اين كاربران ويژه، تنها كاربري به نام Vahid قادر است متد DoSomethingSecure را فراخواني و اجرا كند.

اكنون سؤال اينجا است كه فيلتر Authorize چگونه از دو مكانيزم اعتبار سنجي ياد شده استفاده مي‌كند؟ براي پاسخ به اين سؤال، فايل web.config برنامه را باز نموده و به قسمت authentication آن دقت كنيد:

<authentication mode="Forms">
      <forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>

به صورت پيش فرض، برنامه‌هاي ايجاد شده توسط VS.NET جهت استفاده از حالت Forms يا همان Forms authentication تنظيم شده‌اند. در اينجا كليه كاربران اعتبار سنجي نشده، به كنترلري به نام Account و متد LogOn در آن هدايت مي‌شوند.
براي تغيير آن به حالت اعتبار سنجي يكپارچه با ويندوز، فقط كافي است مقدار mode را به Windows تغيير داد و تنظيمات forms آن‌را نيز حذف كرد.


يك نكته: اعمال تنظيمات اعتبار سنجي اجباري به تمام صفحات سايت
تنظيم زير نيز در فايل وب كانفيگ برنامه، همان كار افزودن ويژگي Authorize را انجام مي‌دهد با اين تفاوت كه تمام صفحات سايت را به صورت خودكار تحت پوشش قرار خواهد داد (البته منهاي loginUrl ايي كه در تنظيمات فوق مشاهده نموديد):

<authorization>
     <deny users="?" />
</authorization>

در اين حالت دسترسي به تمام آدرس‌هاي سايت تحت تاثير قرار مي‌گيرند، منجمله دسترسي به تصاوير و فايل‌هاي CSS و غيره. براي اينكه اين موارد را براي مثال در حين نمايش صفحه لاگين نيز نمايش دهيم، بايد تنظيم زير را پيش از تگ system.web به فايل وب كانفيگ برنامه اضافه كرد:

<!-- we don't want to stop anyone seeing the css and images -->
<location path="Content">
    <system.web>
      <authorization>
        <allow users="*" />
      </authorization>
    </system.web>
</location>

در اينجا پوشه Content از سيستم اعتبارسنجي اجباري خارج مي‌شود و تمام كاربران به آن دسترسي خواهند داشت.
به علاوه امكان امن ساختن تنها قسمتي از سايت نيز ميسر است؛ براي مثال:

<location path="secure">
  <system.web>
    <authorization>
      <allow roles="Administrators" />
      <deny users="*" />
    </authorization>
  </system.web>
</location>

در اينجا مسيري به نام secure، نياز به اعتبارسنجي اجباري دارد. به علاوه تنها كاربراني در نقش Administrators به آن دسترسي خواهند داشت.


نكته: به تنظيمات انجام شده در فايل Web.Config دقت داشته باشيد
همانطور كه مي‌شود دسترسي به يك مسير را توسط تگ location بازگذاشت، امكان بستن آن هم فراهم است (بجاي allow از deny استفاده شود). همچنين در ASP.NET MVC به سادگي مي‌توان تنظيمات مسيريابي را در فايل global.asax.cs تغيير داد. براي مثال اينبار مسير دسترسي به صفحات امن سايت، Admin خواهد بود نه Secure. در اين حالت چون از فيلتر Authorize استفاده نشده و همچنين فايل web.config نيز تغيير نكرده، اين صفحات بدون محافظت رها خواهند شد.
بنابراين اگر از تگ location براي امن سازي قسمتي از سايت استفاده مي‌كنيد، حتما بايد پس از تغييرات مسيريابي، فايل web.config را هم به روز كرد تا به مسير جديد اشاره كند.
به همين جهت در ASP.NET MVC بهتر است كه صريحا از فيلتر Authorize بر روي كنترلرها (جهت اعمال به تمام متدهاي آن) يا بر روي متدهاي خاصي از كنترلرها استفاده كرد.
امكان تعريف AuthorizeAttribute در فايل global.asax.cs و متد RegisterGlobalFilters آن به صورت سراسري نيز وجود دارد. اما در اين حالت حتي صفحه لاگين سايت هم ديگر در دسترس نخواهد بود. براي رفع اين مشكل در ASP.NET MVC 4 فيلتر ديگري به نام AllowAnonymousAttribute معرفي شده است تا بتوان قسمت‌هايي از سايت را مانند صفحه لاگين، از سيستم اعتبارسنجي اجباري خارج كرد تا حداقل كاربر بتواند نام كاربري و كلمه عبور خودش را وارد نمايد:

[System.Web.Mvc.AllowAnonymous]
public ActionResult Login()
{
    return View();
}

بنابراين در ASP.NET MVC 4.0، فيلتر AuthorizeAttribute را سراسري تعريف كنيد. سپس در كنترلر لاگين برنامه از فيلتر AllowAnonymous استفاده نمائيد.
البته نوشتن فيلتر سفارشي AllowAnonymousAttribute در ASP.NET MVC 3.0 نيز ميسر است. براي مثال:

public class LogonAuthorize : AuthorizeAttribute {
     public override void OnAuthorization(AuthorizationContext filterContext) {
         if (!(filterContext.Controller is AccountController))
             base.OnAuthorization(filterContext);
     }
}

در اين فيلتر سفارشي، اگر كنترلر جاري از نوع AccountController باشد، از سيستم اعتبار سنجي اجباري خارج خواهد شد. مابقي كنترلرها همانند سابق پردازش مي‌شوند. به اين معنا كه اكنون مي‌توان LogonAuthorize را به صورت يك فيلتر سراسري در فايل global.asax.cs معرفي كرد تا به تمام كنترلرها، منهاي كنترلر Account اعمال شود.



مثالي جهت بررسي حالت Windows Authentication

يك پروژه جديد خالي ASP.NET MVC را آغاز كنيد. سپس يك كنترلر جديد را به نام Home نيز به آن اضافه كنيد. در ادامه متد Index آن‌را با ويژگي Authorize، مزين نمائيد. همچنين بر روي نام اين متد كليك راست كرده و يك View خالي را براي آن ايجاد كنيد:

using System.Web.Mvc;

namespace MvcApplication15.Controllers
{
    public class HomeController : Controller
    {
        [Authorize]
        public ActionResult Index()
        {
            return View();
        }
    }
}

محتواي View متناظر با متد Index را هم به شكل زير تغيير دهيد تا نام كاربر وارد شده به سيستم را نمايش دهد:

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>
Current user: @User.Identity.Name

به علاوه در فايل Web.config برنامه، حالت اعتبار سنجي را به ويندوز تغيير دهيد:

<authentication mode="Windows" />

اكنون اگر برنامه را اجرا كنيد و وب سرور آزمايشي انتخابي هم IIS Express باشد، پيغام HTTP Error 401.0 - Unauthorized نمايش داده مي‌شود. علت هم اينجا است كه Windows Authentication به صورت پيش فرض در اين وب سرور غيرفعال است. براي فعال سازي آن به مسير My Documents\IISExpress\config مراجعه كرده و فايل applicationhost.config را باز نمائيد. تگ windowsAuthentication را يافته و ويژگي enabled آن‌را كه false است به true تنظيم نمائيد. اكنون اگر برنامه را مجددا اجرا كنيم، در محل نمايش User.Identity.Name، نام كاربر وارد شده به سيستم نمايش داده خواهد شد.
همانطور كه مشاهده مي‌كنيد در اينجا همه چيز يكپارچه است و حتي نيازي نيست صفحه لاگين خاصي را به كاربر نمايش داد. همينقدر كه كاربر توانسته به سيستم ويندوزي وارد شود، بر اين اساس هم مي‌تواند از برنامه‌هاي وب موجود در شبكه استفاده كند.



بررسي حالت Forms Authentication

براي كار با Forms Authentication نياز به محلي براي ذخيره سازي اطلاعات كاربران است. اكثر مقالات را كه مطالعه كنيد شما را به مباحث membership مطرح شده در زمان ASP.NET 2.0 ارجاع مي‌دهند. اين روش در ASP.NET MVC هم كار مي‌كند؛ اما الزامي به استفاده از آن نيست.

براي بررسي حالت اعتبار سنجي مبتني بر فرم‌ها، يك برنامه خالي ASP.NET MVC جديد را آغاز كنيد. يك كنترلر Home ساده را نيز به آن اضافه نمائيد.
سپس نياز است نكته «تنظيمات اعتبار سنجي اجباري تمام صفحات سايت» را به فايل وب كانفيگ برنامه اعمال نمائيد تا نيازي نباشد فيلتر Authorize را در همه جا معرفي كرد. سپس نحوه معرفي پيش فرض Forms authentication تعريف شده در فايل web.config نيز نياز به اندكي اصلاح دارد:

<authentication mode="Forms">
      <!--one month ticket-->
      <forms name=".403MyApp" 
             cookieless="UseCookies" 
             loginUrl="~/Account/LogOn" 
             defaultUrl="~/Home" 
             slidingExpiration="true" 
             protection="All" 
             path="/" 
             timeout="43200"/>
</authentication>

در اينجا استفاده از كوكي‌ها اجباري شده است. loginUrl به كنترلر و متد لاگين برنامه اشاره مي‌كند. defaultUrl مسيري است كه كاربر پس از لاگين به صورت خودكار به آن هدايت خواهد شد. همچنين نكته‌ي مهم ديگري را كه بايد رعايت كرد، name ايي است كه در اين فايل config عنوان مي‌‌كنيد. اگر بر روي يك وب سرور، چندين برنامه وب ASP.Net را در حال اجرا داريد، بايد براي هر كدام از اين‌ها نامي جداگانه و منحصربفرد انتخاب كنيد، در غيراينصورت تداخل رخ داده و گزينه مرا به خاطر بسپار شما كار نخواهد كرد.
كار slidingExpiration كه در اينجا تنظيم شده است نيز به صورت زير مي‌باشد:
اگر لاگين موفقيت آميزي ساعت 5 عصر صورت گيرد و timeout شما به عدد 10 تنظيم شده باشد، اين لاگين به صورت خودكار در 5:10‌ منقضي خواهد شد. اما اگر در اين حين در ساعت 5:05 ، كاربر، يكي از صفحات سايت شما را مرور كند، زمان منقضي شدن كوكي ذكر شده به 5:15 تنظيم خواهد شد(مفهوم تنظيم slidingExpiration). لازم به ذكر است كه اگر كاربر پيش از نصف زمان منقضي شدن كوكي (مثلا در 5:04)، يكي از صفحات را مرور كند، تغييري در اين زمان نهايي منقضي شدن رخ نخواهد داد.
اگر timeout ذكر نشود، زمان منقضي شدن كوكي ماندگار (persistent) مساوي زمان جاري + زمان منقضي شدن سشن كاربر كه پيش فرض آن 30 دقيقه است، خواهد بود.

سپس يك مدل را به نام Account به پوشه مدل‌هاي برنامه با محتواي زير اضافه نمائيد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication15.Models
{
    public class Account
    {
        [Required(ErrorMessage = "Username is required to login.")]
        [StringLength(20)]
        public string Username { get; set; }

        [Required(ErrorMessage = "Password is required to login.")]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        public bool RememberMe { get; set; }
    }
}

همچنين مطابق تنظيمات اعتبار سنجي مبتني بر فرم‌هاي فايل وب كانفيگ، نياز به يك AccountController نيز هست:

using System.Web.Mvc;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public ActionResult LogOn()
        {
            return View();
        }

        [HttpPost]
        public ActionResult LogOn(Account loginInfo, string returnUrl)
        {
            return View();
        }
    }
}

در اينجا در حالت HttpGet فرم لاگين نمايش داده خواهد شد. بنابراين بر روي اين متد كليك راست كرده و گزينه Add view را انتخاب كنيد. سپس در صفحه باز شده گزينه Create a strongly typed view را انتخاب كرده و مدل را هم بر روي كلاس Account قرار دهيد. قالب scaffolding را هم Create انتخاب كنيد. به اين ترتيب فرم لاگين برنامه ساخته خواهد شد.
اگر به متد HttpPost فوق دقت كرده باشيد، علاوه بر دريافت وهله‌اي از شيء Account، يك رشته را به نام returnUrl نيز تعريف كرده است. علت هم اينجا است كه سيستم Forms authentication، صفحه بازگشت را به صورت خودكار به شكل يك كوئري استرينگ به انتهاي Url جاري اضافه مي‌كند. مثلا:

http://localhost/Account/LogOn?ReturnUrl=something

بنابراين اگر يكي از پارامترهاي متد تعريف شده به نام returnUrl باشد، به صورت خودكار مقدار دهي خواهد شد.

تا اينجا زمانيكه برنامه را اجرا كنيم، ابتدا بر اساس تعاريف مسيريابي پيش فرض برنامه، آدرس كنترلر Home و متد Index آن فراخواني مي‌گردد. اما چون در وب كانفيگ برنامه authorization را فعال كرده‌ايم، برنامه به صورت خودكار به آدرس مشخص شده در loginUrl قسمت تعاريف اعتبارسنجي مبتني بر فرم‌ها هدايت خواهد شد. يعني آدرس كنترلر Account و متد LogOn آن درخواست مي‌گردد. در اين حالت صفحه لاگين نمايان خواهد شد.

مرحله بعد، اعتبار سنجي اطلاعات وارد شده كاربر است. بنابراين نياز است كنترلر Account را به نحو زير بازنويسي كرد:

using System.Web.Mvc;
using System.Web.Security;
using MvcApplication15.Models;

namespace MvcApplication15.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public ActionResult LogOn(string returnUrl)
        {
            if (User.Identity.IsAuthenticated) //remember me
            {
                if (shouldRedirect(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                return Redirect(FormsAuthentication.DefaultUrl);
            }

            return View(); // show the login page
        }

        [HttpGet]
        public void LogOut()
        {
            FormsAuthentication.SignOut();
        }

        private bool shouldRedirect(string returnUrl)
        {
            // it's a security check
            return !string.IsNullOrWhiteSpace(returnUrl) &&
                                Url.IsLocalUrl(returnUrl) &&
                                returnUrl.Length > 1 &&
                                returnUrl.StartsWith("/") &&
                                !returnUrl.StartsWith("//") &&
                                !returnUrl.StartsWith("/\\");
        }

        [HttpPost]
        public ActionResult LogOn(Account loginInfo, string returnUrl)
        {
            if (this.ModelState.IsValid)
            {
                if (loginInfo.Username == "Vahid" && loginInfo.Password == "123")
                {
                    FormsAuthentication.SetAuthCookie(loginInfo.Username, loginInfo.RememberMe);
                    if (shouldRedirect(returnUrl))
                    {
                        return Redirect(returnUrl);
                    }
                    FormsAuthentication.RedirectFromLoginPage(loginInfo.Username, loginInfo.RememberMe);
                }
            }
            this.ModelState.AddModelError("", "The user name or password provided is incorrect.");
            ViewBag.Error = "Login faild! Make sure you have entered the right user name and password!";
            return View(loginInfo);
        }
    }
}

در اينجا با توجه به گزينه «مرا به خاطر بسپار»، اگر كاربري پيشتر لاگين كرده و كوكي خودكار حاصل از اعتبار سنجي مبتني بر فرم‌هاي او نيز معتبر باشد، مقدار User.Identity.IsAuthenticated مساوي true خواهد بود. بنابراين نياز است در متد LogOn از نوع HttpGet به اين مساله دقت داشت و كاربر اعتبار سنجي شده را به صفحه پيش‌فرض تعيين شده در فايل web.config برنامه يا returnUrl هدايت كرد.
در متد LogOn از نوع HttpPost، كار اعتبارسنجي اطلاعات ارسالي به سرور انجام مي‌شود. در اينجا فرصت خواهد بود تا اطلاعات دريافتي، با بانك اطلاعاتي مقايسه شوند. اگر اطلاعات مطابقت داشتند، ابتدا كوكي خودكار FormsAuthentication تنظيم شده و سپس به كمك متد RedirectFromLoginPage كاربر را به صفحه پيش فرض سيستم هدايت مي‌كنيم. يا اگر returnUrl ايي وجود داشت، آن‌را پردازش خواهيم كرد.
براي پياده سازي خروج از سيستم هم تنها كافي است متد FormsAuthentication.SignOut فراخواني شود تا تمام اطلاعات سشن و كوكي‌هاي مرتبط، به صورت خودكار حذف گردند.

تا اينجا فيلتر Authorize بدون پارامتر و همچنين در حالت مشخص سازي صريح كاربران به نحو زير را پوشش داديم:

[Authorize(Users="Vahid")]

اما هنوز حالت استفاده از Roles در فيلتر Authorize باقي مانده است. براي فعال سازي خودكار بررسي نقش‌هاي كاربران نياز است يك Role provider سفارشي را با پياده سازي كلاس RoleProvider، طراحي كنيم. براي مثال:

using System;
using System.Web.Security;

namespace MvcApplication15.Helper
{
    public class CustomRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            if (username.ToLowerInvariant() == "ali" && roleName.ToLowerInvariant() == "user")
                return true;
            // blabla ...
            return false;
        }

        public override string[] GetRolesForUser(string username)
        {
            if (username.ToLowerInvariant() == "ali")
            {
                return new[] { "User", "Helpdesk" };
            }

            if(username.ToLowerInvariant()=="vahid")
            {
                return new [] { "Admin" };
            }

            return new string[] { };
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }        

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }        

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }
    }
}

در اينجا حداقل دو متد IsUserInRole و GetRolesForUser بايد پياده سازي شوند و مابقي اختياري هستند.
بديهي است در يك برنامه واقعي اين اطلاعات بايد از يك بانك اطلاعاتي خوانده شوند؛ براي نمونه به ازاي هر كاربر تعدادي نقش وجود دارد. به ازاي هر نقش نيز تعدادي كاربر تعريف شده است (يك رابطه many-to-many بايد تعريف شود).
در مرحله بعد بايد اين Role provider سفارشي را در فايل وب كانفيگ برنامه در قسمت system.web آن تعريف و ثبت كنيم:

<roleManager>
      <providers>
        <clear />
        <add name="CustomRoleProvider" type="MvcApplication15.Helper.CustomRoleProvider"/>
      </providers>
    </roleManager>


همين مقدار براي راه اندازي بررسي نقش‌ها در ASP.NET MVC كفايت مي‌كند. اكنون امكان تعريف نقش‌ها، حين بكارگيري فيلتر Authorize ميسر است:

[Authorize(Roles = "Admin")]
public class HomeController : Controller