مطالب
Delegate در سی شارپ
یک Delegate نوعی اشاره‌گر است به توابع در سی شارپ که می‌تواند ارجاعی را به یک یا چند تابع بخصوص داشته باشد. منظور از توابع در سی شارپ، متدها هستند. امضای یک Delegate باید با متدی که به آن اشاره می‌کنید یکی باشد.
using System;
using System.Windows.Forms;
 
namespace CSharpDelegates
{
    public delegate void Display(string sMsg);
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            Display del = new Display(ShowMessage);
            del("This is an example for delegate");
        }
 
        private void ShowMessage(string strMessage)
        {
            MessageBox.Show(strMessage);
        }
 
    }
}
  همانطور که در کد بالا مشاهده می‌کنید، Delegate‌ها بسیار شبیه به کلاس‌ها هستند. می‌توانیم از آنها یک شیء ساخته و نام متدی را که قرار است به آن اشاره کند، از طریق سازنده به آن ارسال کنیم. در کد بالا یک Delegate را با نام Display ساخته‌ایم که به متد ShowMessage اشاره می‌کند. اگر به Delegate و متد ShowMessage دقت کنید خواهید دید که هر دو دارای پارامتر ورودی و امضای یکسانی هستند. ما شیءای به نام Display را از نوع Delegate ساخته‌ایم که متدی به نام ShowMessage را با پارامتر ورودی از نوع string، اجرا می‌کند.
شاید بپرسید که چرا باید از Delegate استفاده کنیم؟ چرا متد ShowMessage را مستقیما اجرا نکنیم؟
خوب، Delegate‌ها برای طراحی فریم ورکهایی با قابلیت استفاده‌ی مجدد از کدهای آنها، بسیار مناسب هستند. بگذارید این مطلب را با یک مثال ساده از کلاس Employee توضیح دهیم.
ویژال استودیو را باز کنید و یک پروژه‌ی Windows Forms Application ساده را با نام CSharpDelegates بسازید. سپس کلاس زیر را به آن اضافه کنید:   
using System.Collections.Generic;
 
namespace CSharpDelegates
{
    public class Employee
    {
        public int EmployeeId { get; set; }
 
        public string Name { get; set; }
 
        public int Experience { get; set; }
 
        public double Salary { get; set; }
 
        public void IncreaseSalary(List<Employee> Employees)
        {
            foreach (Employee emp in Employees)
            {
                if (emp.Salary < 10000)
                {
                    emp.Salary = emp.Salary + emp.Salary * 0.3;
                }
            }
        }
    }
}
در کلاس Employee بالا، تعدادی فیلد و یک متد با نام IncreaseSalary داریم که وظیفه‌ی آن افزایش 30% حقوق کارمندانی است که کمتر از 10000 می‌گیرند. اگر در آینده قصد داشته باشیم که علاوه بر این افزایش حقوق، منطق دیگری را با میزان ترفیع و شایستگی کارمندان نیز لحاظ کنیم، لازم است کدهای متد IncreaseSalary را تغییر دهیم که این کار، یک کار خسته کننده است و شاید ما دوست نداشته باشیم تا کدهای کلاس پایه‌ی Employee را تغییر دهیم. در این نوع سناریوها می‌توان با استفاده از Delegateها، منطق افزایش حقوق و منطق ترفیع و شایستگی کارمندان را از هم جدا کرد. خوب، اولین کار، ویرایش متد IncreaseSalary است:  
using System.Collections.Generic;
 
namespace CSharpDelegates
{
    public delegate bool SalaryIncreaseEligibility(Employee emp);
    public class Employee
    {
        public int EmployeeId { get; set; }
 
        public string Name { get; set; }
 
        public int Experience { get; set; }
 
        public double Salary { get; set; }
 
        public string IncreaseSalary(List<Employee> Employees, SalaryIncreaseEligibility del)
        {
            string sSalIncreasdEmployees = "Salary increased for ";
            foreach (Employee emp in Employees)
            {
                if (del(emp))
                {
                    emp.Salary = emp.Salary + emp.Salary * 0.3;
                    sSalIncreasdEmployees = sSalIncreasdEmployees + emp.Name + " ,";
                }
            }
 
            return sSalIncreasdEmployees;
        }
    }
}
همانطور که در کد بالا قابل مشاهده است، منطق افزایش حقوق بر اساس ترفیع و شایستگی کارمندان را با Delegate ایی به نام SalaryIncreaseEligibility جدا کرده‌ایم. بدین وسیله می‌توانیم منطق شناسایی کردن کارمندان لایق افزایش حقوق را بدون ایجاد تغییری در کلاس Employee سفارشی کنیم. حال بگذارید متد IncreaseSalary از کلاس Employee را با منطق سفارشی خود برای افزایش حقوق کارمندان لایق، با کمک Delegate ایی به نام SalaryIncreaseEligibility اجرا کنیم. 
using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace CSharpDelegates
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            List<Employee> empList = new List<Employee>();
            empList.Add(new Employee() { EmployeeId = 100, Name = "Mark", Salary = 2000, Experience = 3 });
            empList.Add(new Employee() { EmployeeId = 101, Name = "John", Salary = 15000, Experience = 8 });
            empList.Add(new Employee() { EmployeeId = 102, Name = "David", Salary = 4000, Experience = 4 });
            empList.Add(new Employee() { EmployeeId = 103, Name = "Bob", Salary = 50000, Experience = 14 });
            empList.Add(new Employee() { EmployeeId = 104, Name = "Alex", Salary = 9000, Experience = 6 });
 
            SalaryIncreaseEligibility del = new SalaryIncreaseEligibility(SalaryEligibility);
 
            Employee objEmp = new Employee();
            string sMsg = objEmp.IncreaseSalary(empList, del);
 
            MessageBox.Show(sMsg);
        }
 
        private bool SalaryEligibility(Employee emp)
        {
            if (emp.Salary > 10000)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
 
    }
}
در کد بالا ما منطق ترفیع و شایستگی کارمندان را از متد SalaryEligibility جدا کرده‌ایم و این منطق را به کمک Delegate ای به نام SalaryIncreaseEligibility به متد ذکر شده پاس داده‌ایم. در آینده اگر قصد داشته باشیم تا این افزایش حقوق را بر اساس منطق دیگری تعریف کنیم، فقط کافیست که متد SalaryEligibility را تغییر دهیم و دیگر لازم نیست تغییری در کلاس Employee ایجاد کنیم.
مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت سوم - اعتبارسنجی سمت کلاینت
FluentValidation یک کتابخانه‌ی اعتبارسنجی اطلاعات سمت سرور است و راهکاری را برای اعتبارسنجی‌های سمت کلاینت ارائه نمی‌دهد؛ اما می‌تواند متادیتای مورد نیاز unobtrusive java script validation را تولید کند و به این ترتیب بسیاری از قواعد آن دقیقا همانند روش‌های توکار اعتبارسنجی ASP.NET Core در سمت کلاینت نیز کار می‌کنند؛ مانند:
• NotNull/NotEmpty
• Matches (regex)
• InclusiveBetween (range)
• CreditCard
• Email
• EqualTo (cross-property equality comparison)
• MaxLength
• MinLength
• Length

اما باید دقت داشت که سایر قابلیت‌های این کتابخانه، قابلیت ترجمه‌ی به قواعد unobtrusive java script validation ندارند؛ مانند:
- استفاده‌ی از شرط‌ها توسط متدهای When و یا Unless (برای مثال اگر خاصیت A دارای مقدار 5 بود، آنگاه ورود اطلاعات خاصیت B باید اجباری شود)
- تعریف قواعد اعتبارسنجی سفارشی، برای مثال توسط متد Must
- تعریف RuleSetها (برای مثال تعیین کنید که از یک مدل، فقط تعدادی از خواص آن اعتبارسنجی شوند و نه تمام آن‌ها)

در یک چنین حالت‌هایی باید کاربر اطلاعات کامل فرم را به سمت سرور ارسال کند و در post-back صورت گرفته، نتایج اعتبارسنجی را دریافت کند.


روش توسعه‌ی اعتبارسنجی سمت کلاینت کتابخانه‌ی FluentValidation جهت سازگاری آن با  unobtrusive java script validation

باتوجه به اینکه قواعد سفارشی کتابخانه‌ی FluentValidation، معادل unobtrusive java script validation ای ندارند، در ادامه مثالی را جهت تدارک آن‌ها بررسی می‌کنیم. برای این منظور، معادل مطلب «ایجاد ویژگی‌های اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آن‌ها» را بر اساس ویژگی‌های کتابخانه‌ی FluentValidation پیاده سازی می‌کنیم.

1) ایجاد اعتبارسنج سمت سرور

می‌خواهیم اگر مقدار عددی خاصیتی بیشتر از مقدار عددی خاصیت دیگری بود، اعتبارسنجی آن با شکست مواجه شود. به همین منظور با پیاده سازی یک PropertyValidator سفارشی، LowerThanValidator را به صورت زیر تعریف می‌کنیم:
using System;
using FluentValidation;
using FluentValidation.Validators;

namespace FluentValidationSample.Models
{
    public class LowerThanValidator : PropertyValidator
    {
        public string DependentProperty { get; set; }

        public LowerThanValidator(string dependentProperty) : base($"باید کمتر از {dependentProperty} باشد")
        {
            DependentProperty = dependentProperty;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            if (context.PropertyValue == null)
            {
                return false;
            }

            var typeInfo = context.Instance.GetType();
            var dependentPropertyValue =
                Convert.ToInt32(typeInfo.GetProperty(DependentProperty).GetValue(context.Instance, null));

            return int.Parse(context.PropertyValue.ToString()) < dependentPropertyValue;
        }
    }

    public static class CustomFluentValidationExtensions
    {
        public static IRuleBuilderOptions<T, int> LowerThan<T>(
            this IRuleBuilder<T, int> ruleBuilder, string dependentProperty)
        {
            return ruleBuilder.SetValidator(new LowerThanValidator(dependentProperty));
        }
    }
}
- در اینجا dependentProperty همان خاصیت دیگری است که باید مقایسه با آن صورت گیرد. در متد بازنویسی شده‌ی IsValid، می‌توان مقدار آن‌را از context.Instance استخراج کرده و سپس با مقدار جاری context.PropertyValue مقایسه کرد.
- همچنین جهت سهولت کار فراخوانی آن، یک متد الحاقی جدید به نام LowerThan نیز در اینجا تعریف شده‌است.
- البته اگر قصد اعتبارسنجی سمت سرور را ندارید، می‌توان در متد IsValid، مقدار true را بازگشت داد.


2) ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.Models
{
    public class LowerThanClientValidator : ClientValidatorBase
    {
        private LowerThanValidator LowerThanValidator
        {
            get { return (LowerThanValidator)Validator; }
        }

        public LowerThanClientValidator(PropertyRule rule, IPropertyValidator validator) :
            base(rule, validator)
        {
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context));
            MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", LowerThanValidator.DependentProperty);
        }

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر یک چنین تگ HTML ای می‌شود که در آن پیام اعتبارسنجی و خاصیت دیگر مدنظر ذکر شده‌اند:
<input dir="rtl" type="number"
 data-val="true"
 data-val-lowerthan="باید کمتر از Age باشد"
 data-val-lowerthan-dependentproperty="Age"
 data-val-required="'سابقه کار' must not be empty."
 id="Experience"
 name="Experience"
 value="">


3) تعریف مدل کاربران و اعتبارسنجی آن

مدلی که در این مثال از آن استفاده شده، یک چنین تعریفی را دارد و در قسمت اعتبارسنجی خاصیت Experience آن، از متد الحاقی جدید LowerThan استفاده شده‌است:
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

        [Display(Name = "سن")]
        public int Age { get; set; }

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }
    }

    public class UserValidator : AbstractValidator<UserModel>
    {
        public UserValidator()
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
        }
    }


4) افزودن اعتبارسنج‌های تعریف شده به تنظیمات برنامه

پس از تعریف LowerThanValidator و LowerThanClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        clientSideValidation.Add(
                            validatorType: typeof(LowerThanValidator),
                            factory: (context, rule, validator) => new LowerThanClientValidator(rule, validator));
                    });
                }
            );
        }


5) تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه فایل js/site.js را به صورت زیر تکمیل خواهیم کرد:
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) {
    options.rules['LowerThan'] = {
        dependentproperty: options.params['dependentproperty']
    };
    options.messages['LowerThan'] = options.message;
});

$.validator.addMethod('LowerThan', function (value, element, parameters) {      
    var dependentProperty = '#' + parameters['dependentproperty'];
    var dependentControl = $(dependentProperty);
    if (dependentControl) {
        var targetvalue = dependentControl.val();
        if (parseInt(targetvalue) > parseInt(value)) {
            return true;
        }
        return false;
    }
    return true;
});
در اینجا یکبار اعتبارسنج LowerThan را به validator.unobtrusive معرفی می‌کنیم. سپس منطق پیاده سازی آن‌را که بر اساس یافتن مقدار خاصیت دیگر و مقایسه‌ی آن با مقدار خاصیت جاری است، به اعتبارسنج‌های jQuery Validator اضافه خواهیم کرد.


6) آزمایش برنامه

پس از این تنظیمات، اگر Viewما چنین تعریفی را داشته باشد:
@using FluentValidationSample.Models
@model UserModel
@{
    ViewData["Title"] = "Home Page";
}
<div dir="rtl">
    <form asp-controller="Home"
          asp-action="RegisterUser"
          method="post">
        <fieldset class="form-group">
        <legend>ثبت نام</legend>
            <div class="form-group row">
                <label asp-for="Username" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Username" class="form-control" />
                    <span asp-validation-for="Username" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Age" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Age" class="form-control" />
                    <span asp-validation-for="Age" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group row">
                <label asp-for="Experience" class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10">
                    <input dir="rtl" asp-for="Experience" class="form-control" />
                    <span asp-validation-for="Experience" class="text-danger"></span>
                </div>
            </div>

            <div class="form-group row">
                <label class="col-md-2 col-form-label text-md-left"></label>
                <div class="col-md-10 text-md-right">
                    <button type="submit" class="btn btn-info col-md-2">ارسال</button>
                </div>
            </div>
        </fieldset>
    </form>
</div>
در صورتیکه سابقه‌ی کار را بیشتر از سن وارد کنیم، به یک چنین خروجی سمت کلاینتی (بدون نیاز به post-back کامل به سمت سرور) خواهیم رسید:

نظرات مطالب
پشتیبانی از Generic Attributes در C# 11
روش تبدیل ServiceFilterAttribute در ASP.NET Core به یک نمونه‌ی Type-Safe

این ویژگی در اصل به صورت زیر تعریف شده‌است:
/// <summary>
/// Instantiates a new <see cref="ServiceFilterAttribute"/> instance.
/// </summary>
/// <param name="type">The <see cref="Type"/> of filter to find.</param>
public ServiceFilterAttribute(Type type)
{
     ServiceType = type ?? throw new ArgumentNullException(nameof(type));
}
و به همین جهت در زمان کامپایل، محدودیتی در مورد نوع ارسالی به آن وجود ندارد. می‌توان این مشکل را در C# 11 با استفاده از قیود جنریک، به نحو زیر برطرف کرد:
public class TypeSafeServiceFilterAttribute<T> : ServiceFilterAttribute where T: IActionFilter
{
        public TypeSafeServiceFilterAttribute():base(typeof(T))
        {
            
        }
}
در این حالت در حین استفاده‌ی از آن بر روی یک اکشن متد:
 [Route("api/[controller]")]
 [ApiController]
 public class CoursesController : ControllerBase
 {
        [HttpGet]
        [TypeSafeServiceFilterAttribute<ExampleFilter>()]
        public IActionResult Get()
        {
            return Ok();
        }
 }
اگر ExampleFilter مورد استفاده، از نوع IActionFilter نباشد، برنامه کامپایل نخواهد شد.
مطالب
بازسازی کد: جایگزینی متد با شیء (Replace method with method object)
زیبایی متدهای کوچک، در قابل درک بودن آنها نهفته است. هدف نهایی بیشتر روش‌های بازسازی کد، تغییر تکه کدی بزرگ و در هم تنیده، به تکه‌های کوچک قابل درک‌تر است. مهمترین مانع در شکستن تکه کدهای طولانی، وجود متغیرهای موقتی در آن است. چند روش بازسازی کد مطرح شده به طور خاص به این موضوع توجه دارند. 
زمانیکه متد طولانی ای در کد وجود داشته باشد که نتوان (به راحتی) آن را به چند متد کوچکتر تقسیم کرد، معمولا از این روش بازسازی کد استفاده می‌شود. 
در این روش می‌توان کلاسی را تعریف کرد و متد مربوطه را به آن کلاس منتقل کرد. در نتیجه متغیرهای حاضر در متد را می‌توان در کلاس، به فیلدهای اطلاعاتی تبدیل کرد. پس از این تغییر حتی می‌توان فراتر رفت و متد ابتدایی را به راحتی به چند متد معنی دار در کلاس مقصد تبدیل کرد. 
فرض کنید در کلاس زیر متدی نسبتا طولانی برای محاسبه قیمت سفارش وجود دارد.  
public class Order { 
    public decimal Price() { 
        var primaryBasePrice = 10000; 
        var branchDiscountBasePrice = 9000; 
        var taxPercentage = 9; 
        // calculate price 
    } 
}
متدی مانند متد Price معمولا پیچیده‌تر از موردی است که در این مثال آورده شده است. علارغم این، متد ذکر شده با توجه به متغیرهای محلی موقتی ای که دارد و روالی که در آن نیاز به پیاده سازی است، ممکن است به اندازه‌ای به متغیرهای مربوطه وابسته باشد که نتوان آن را به متدهای کوچکتر در همین کلاس تقسیم کرد و یا تقسیم کردن آن در همین کلاس کمک چندانی به ما نکند. در چنین شرایطی بازسازی کد مورد نظر این نوشتار انجام می‌شود و کد، شکلی شبیه به تکه کد زیر را به خود می‌گیرد. 
public class Order 
{ 
    public decimal Price() { 
        return new PriceCalculator(this).Compute(); 
    } 
} 
public class PriceCalculator 
{ 
    private Order orderInstance ;
    public PriceCalculator(Order order) 
    { 
        orderInstance = order;
    } 
    public decimal Compute() 
    { 
        var primaryBasePrice = 10000; 
        var branchDiscountBasePrice = 9000; 
        var taxPercentage = 9; 
        // calculate price 
    } 
}

در بازسازی ذکر شده، کلاسی با نام PriceCalculator برای روال محاسبه قیمت ایجاد شده و متدی با نام Compute در آن ساخته شده است که محاسبه قیمت را انجام می‌دهد. به دلیل این که ممکن است در کد مربوط به محاسبه قیمت، از خصوصیات موجود در کلاس Order استفاده شود، شیء مربوطه نیز به صورت پارامتر به کلاس محاسبه قیمت پاس داده می‌شود. 
همانطور که در ابتدای مطلب ذکر شد، بعد از این بازسازی کد می‌توان متغیرهای محلی موقتی را به صورت فیلدهای کلاس PriceCalculator تغییر داد و حتی متد را با سادگی بیشتری به چند متد تقسیم کرد. به طور مثال همچنین می‌توان روالهایی مانند محاسبه تخفیف و مالیات را نیز با استفاده از الگوی طراحی decorator در PriceCalculator پیاده سازی نمود.  

مراحل انجام این بازسازی کد  

  1. کلاس جدیدی بسازید و نام آن را بر اساس نام متد مورد نظر خود انتخاب نمایید. 
  2. به کلاس ساخته شده فیلدی برای نگه داری شیء کلاس مرجع (در مثال ما Order) متد اصلی ایجاد کنید.
  3. به کلاس جدید سازنده‌ای برای پاس دادن شیء کلاس مرجع (در مثال ما Order) ایجاد کنید. 
  4. در کلاس جدید متدی با نام مناسب (در مثال ما Compute) ایجاد کنید. 
  5. بدنه متد مورد نظر را به بدنه متد ایجاد شده در کلاس جدید کپی کنید و تمامی رفرنسها به اعضای کلاس مرجع را با رفرنس به شیء موجود آن تغییر دهید. 
  6. کد را کامپایل کنید. 
  7. فراخوانی متد اصلی را با فراخوانی متد شیء کلاس جدید ایجاد شده تغییر دهید.  
یکی از فواید مهم این بازسازی کد، جداسازی منطقی از کد که به صورت متد بوده و تبدیل آن به کلاس است. زمانیکه روالی به کلاس مستقلی تبدیل می‌شود، می‌توان از الگوهای طراحی دیگری برای بهبود بیشتر آن استفاده نمود. همچنین انتقال و استفاده دوباره آن نیز بسیار آسان‌تر خواهد شد. 
نظرات مطالب
RadioButtonList در ASP.NET MVC

من که کدی نمی‌بینم اینجا برای دیباگ و بررسی: آناتومی یک گزارش خطای خوب

ولی احتمالا نام خاصیت ViewModel شما با نام گروهی که تعریف کردید یکی نیست چون در مثال اول بین پارامتر string tech و کلاس زیر فرقی نیست:

public class SomeClass
{
  public string Tech {set; get;}
}
مطالب دوره‌ها
استفاده از Delegates بجای اینترفیس‌ها در تزریق وابستگی‌ها
عموما در معکوس سازی مسئولیت‌ها و واگذاری آن‌ها به لایه‌های دیگر، از اینترفیس‌ها استفاده می‌شود. روش دیگری را که در اینجا می‌توان بکار گرفته استفاده از Delegates است بجای اینترفیس‌ها. از این جهت که یک Delegate در عمل می‌تواند به صورت یک Anonymous Interface عمل کند.
در بسیاری از مواردی که اینترفیس شما تنها از یک متد تشکیل می‌شود، می‌توان عملکرد آن‌را با یک Delagate جهت ساده سازی فرآیند تزریق وابستگی‌ها تعویض کرد.

یک مثال از تعویض اینترفیس‌های تک متدی با Delegates

public interface IAuthentication
{
    bool IsUserAuthenticated(string userName, string password);
}

public class AuthenticationService : IAuthentication
{
    public bool IsUserAuthenticated(string userName, string password)
    {
        return userName == "Vahid" && password == "123";
    }
}

public class LoginController
{
    private readonly IAuthentication _authentication;
    public LoginController(IAuthentication authentication)
    {
        _authentication = authentication;
    }

    // ...
}

مثال بسیار متداول فوق را درنظر بگیرید. در لایه سرویس برنامه، اینترفیس و کلاس پیاده سازی کننده منطق اعتبارسنجی کاربران را تدارک دیده‌ایم. نهایتا جایی در سطحی بالاتر از این توانمندی قرار است استفاده شود. مثلا در کلاس LoginController.
نکته مهم اینترفیس IAuthentication، تک متدی بودن آن است. به همین جهت تعریف کلاس LoginController را به شکل زیر نیز می‌توان بازنویسی کرد:
public class LoginController
{
    private readonly Func<string, string, bool> _authenticationStrategy;
    public LoginController(Func<string, string, bool> authenticationStrategy)
    {
        _authenticationStrategy = authenticationStrategy;
    }

    // ...
}
در این حالت نیز کلاس LoginController استفاده کننده از Delegate تعریف شده، از نحوه و استراتژی اعتبارسنجی بی‌خبر است و پیاده سازی آن به کلاسی دیگر که قرار است از LoginController استفاده کند، واگذار می‌شود.
مطالب
Globalization در ASP.NET MVC
اگر بازار هدف یک محصول شامل چندین کشور، منطقه یا زبان مختلف باشد، طراحی و پیاده سازی آن برای پشتیبانی از ویژگی‌های چندزبانه یک فاکتور مهم به حساب می‌آید. یکی از بهترین روشهای پیاده سازی این ویژگی در دات نت استفاده از فایلهای Resource است. درواقع هدف اصلی استفاده از فایلهای Resource نیز Globalization است. Globalization برابر است با Internationalization + Localization که به اختصار به آن g11n میگویند. در تعریف، Internationalization (یا به اختصار i18n) به فرایند طراحی یک محصول برای پشتیبانی از فرهنگ(culture)‌ها و زبانهای مختلف و Localization (یا L10n) یا بومی‌سازی به شخصی‌سازی یک برنامه برای یک فرهنگ یا زبان خاص گفته میشود. (اطلاعات بیشتر در اینجا).
استفاده از این فایلها محدود به پیاده سازی ویژگی چندزبانه نیست. شما میتوانید از این فایلها برای نگهداری تمام رشته‌های موردنیاز خود استفاده کنید. نکته دیگری که باید بدان اشاره کرد این است که تقرببا تمامی منابع مورد استفاده در یک محصول را میتوان درون این فایلها ذخیره کرد. این منابع در حالت کلی شامل موارد زیر است:
- انواع رشته‌های مورد استفاده در برنامه چون لیبل‌ها و پیغام‌ها و یا مسیرها (مثلا نشانی تصاویر یا نام کنترلرها و اکشنها) و یا حتی برخی تنظیمات ویژه برنامه (که نمیخواهیم براحتی قابل نمایش یا تغییر باشد و یا اینکه بخواهیم با تغییر زبان تغییر کنند مثل direction و امثال آن)
- تصاویر و آیکونها و یا فایلهای صوتی و انواع دیگر فایل ها
- و ...
 نحوه بهره برداری از فایلهای Resource در دات نت، پیاده سازی نسبتا آسانی را در اختیار برنامه نویس قرار میدهد. برای استفاده از این فایلها نیز روشهای متنوعی وجود دارد که در مطلب جاری به چگونگی استفاده از آنها در پروژه‌های ASP.NET MVC پرداخته میشود.

Globalization در دات نت
فرمت نام یک culture دات نت (که در کلاس CultureInfo پیاده شده است) بر اساس استاندارد RFC 4646 (^ و ^) است. (در اینجا اطلاعاتی راجع به RFC یا Request for Comments آورده شده است). در این استاندارد نام یک فرهنگ (کالچر) ترکیبی از نام زبان به همراه نام کشور یا منطقه مربوطه است. نام زبان برپایه استاندارد ISO 639 که یک عبارت دوحرفی با حروف کوچک برای معرفی زبان است مثل fa برای فارسی و en برای انگلیسی و نام کشور یا منطقه نیز برپایه استاندارد ISO 3166 که یه عبارت دوحرفی با حروف بزرگ برای معرفی یک کشور یا یک منطقه است مثل IR برای ایران یا US برای آمریکاست. برای نمونه میتوان به fa-IR برای زبان فارسی کشور ایران و یا en-US برای زبان انگلیسی آمریکایی اشاره کرد. البته در این روش نامگذاری یکی دو مورد استثنا هم وجود دارد (اطلاعات کامل کلیه زبانها: National Language Support (NLS) API Reference). یک فرهنگ خنثی (Neutral Culture) نیز تنها با استفاده از دو حرف نام زبان و بدون نام کشور یا منطقه معرفی میشود. مثل fa برای فارسی یا de برای آلمانی. در این بخش نیز دو استثنا وجود دارد (^).
در دات نت دو نوع culture وجود دارد: Culture و UICulture. هر دوی این مقادیر در هر Thread مقداری منحصربه فرد دارند. مقدار Culture بر روی توابع وابسته به فرهنگ (مثل فرمت رشته‌های تاریخ و اعداد و پول) تاثیر میگذارد. اما مقدار UICulture تعیین میکند که سیستم مدیریت منابع دات نت (Resource Manager) از کدام فایل Resource برای بارگذاری داده‌ها استفاده کند. درواقع در دات نت با استفاده از پراپرتی‌های موجود در کلاس استاتیک Thread برای ثرد جاری (که عبارتند از CurrentCulture و CurrentUICulture) برای فرمت کردن و یا انتخاب Resource مناسب تصمیم گیری میشود. برای تعیین کالچر جاری به صورت دستی میتوان بصورت زیر عمل کرد:
Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture("fa-IR");
دراینجا باید اشاره کنم که کار انتخاب Resource مناسب با توجه به کالچر ثرد جاری توسط ResourceProviderFactory پیشفرض دات نت انجام میشود. در مطالب بعدی به نحوه تعریف یک پرووایدر شخصی سازی شده هم خواهم پرداخت.

پشتیبانی از زبانهای مختلف در MVC
برای استفاده از ویژگی چندزبانه در MVC دو روش کلی وجود دارد.
1. استفاده از فایلهای Resource برای تمامی رشته‌های موجود
2. استفاده از View‌های مختلف برای هر زبان
البته روش سومی هم که از ترکیب این دو روش استفاده میکند نیز وجود دارد. انتخاب روش مناسب کمی به سلیقه‌ها و عادات برنامه نویسی بستگی دارد. اگر فکر میکنید که استفاده از ویوهای مختلف به دلیل جداسازی مفاهیم درگیر در کالچرها (مثل جانمایی اجزای مختلف ویوها یا بحث Direction) باعث مدیریت بهتر و کاهش هزینه‌های پشتیبانی میشود بهتر است از روش دوم یا ترکیبی از این دو روش استفاده کنید. خودم به شخصه سعی میکنم از روش اول استفاده کنم. چون معتقدم استفاده از ویوهای مختلف باعث افزایش بیش از اندازه حجم کار میشود. اما در برخی موارد استفاده از روش دوم یا ترکیبی از دو روش میتواند بهتر باشد.

تولید فایلهای Resource
بهترین مکان برای نگهداری فایلهای Resource در یک پروژه جداگانه است. در پروژه‌های از نوع وب‌سایت پوشه‌هایی با نام App_GlobalResources یا App_LocalResources وجود دارد که میتوان از آنها برای نگهداری و مدیریت این نوع فایلها استفاده کرد. اما همانطور که در اینجا توضیح داده شده است این روش مناسب نیست. بنابراین ابتدا یک پروژه مخصوص نگهداری فایلهای Resource ایجاد کنید و سپس اقدام به تهیه این فایلها نمایید. سعی کنید که عنوان این پروژه به صورت زیر باشد. برای کسب اطلاعات بیشتر درباره نحوه نامگذاری اشیای مختلف در دات نت به این مطلب رجوع کنید.
<SolutionName>.Resources
برای افزودن فایلهای Resource به این پروژه ابتدا برای انتخاب زبان پیش فرض محصول خود تصمیم بگیرید. پیشنهاد میکنم که از زبان انگلیسی (en-US) برای اینکار استفاده کنید. ابتدا یک فایل Resource (با پسوند resx.) مثلا با نام Texts.resx به این پروژه اضافه کنید. با افزودن این فایل به پروژه، ویژوال استودیو به صورت خودکار یک فایل cs. حاوی کلاس متناظر با این فایل را به پروژه اضافه میکند. این کار توسط ابزار توکاری به نام ResXFileCodeGenerator انجام میشود. اگر به پراپرتی‌های این فایل resx. رجوع کنید میتوانید این عنوان را در پراپرتی Custom Tool ببینید. البته ابزار دیگری برای تولید این کلاسها نیز وجود دارد. این ابزارهای توکار برای سطوح دسترسی مخنلف استفاده میشوند. ابزار پیش فرض در ویژوال استودیو یعنی همان ResXFileCodeGenerator، این کلاسها را با دسترسی internal تولید میکند که مناسب کار ما نیست. ابزار دیگری که برای اینکار درون ویژوال استودیو وجود دارد PublicResXFileCodeGenerator است و همانطور که از نامش پیداست از سطح دسترسی public استفاده میکند. برای تغییر این ابزار کافی است تا عنوان آن را دقیقا در پراپرتی Custom Tool تایپ کنید.

نکته: درباره پراپرتی مهم Build Action این فایلها در مطالب بعدی بیشتر بحث میشود.
برای تعیین سطح دسترسی Resource موردنظر به روشی دیگر، میتوانید فایل Resource را باز کرده و Access Modifier آن را به Public تغییر دهید.

سپس برای پشتیبانی از زبانی دیگر، یک فایل دیگر Resource به پروژه اضافه کنید. نام این فایل باید همنام فایل اصلی به همراه نام کالچر موردنظر باشد. مثلا برای زبان فارسی عنوان فایل باید Texts.fa-IR.resx یا به صورت ساده‌تر برای کالچر خنثی (بدون نام کشور) Texts.fa.resx باشد. دقت کنید اگر نام فایل را در همان پنجره افزودن فایل وارد کنید ویژوال استودیو این همنامی را به صورت هوشمند تشخیص داده و تغییراتی را در پراپرتی‌های پیش فرض فایل Resource ایجاد میکند.
نکته: این هوشمندی مرتبه نسبتا بالایی دارد. بدین صورت که تنها درصورتیکه عبارت بعد از نام فایل اصلی Resource (رشته بعد از نقطه مثلا fa در اینجا) متعلق به یک کالچر معتبر باشد این تغییرات اعمال خواهد شد.
مهمترین این تغییرات این است که ابزاری را برای پراپرتی Custom Tool این فایلها انتخاب نمیکند! اگر به پراپرتی فایل Texts.fa.resx مراجعه کنید این مورد کاملا مشخص است. در نتیجه دیگر فایل cs. حاوی کلاسی جداگانه برای این فایل ساخته نمیشود. همچنین اگر فایل Resource جدید را باز کنید میبنید که برای Access Modifier آن گزینه No Code Generation انتخاب شده است.
در ادامه شروع به افزودن عناوین موردنظر در این دو فایل کنید. در اولی (بدون نام زبان) رشته‌های مربوط به زبان انگلیسی و در دومی رشته‌های مربوط به زبان فارسی را وارد کنید. سپس در هرجایی که یک لیبل یا یک رشته برای نمایش وجود دارد از این کلیدهای Resource استفاده کنید مثل:
<SolutionName>.Resources.Texts.Save
<SolutionName>.Resources.Texts.Cancel

استفاده از Resource در ویومدل ها
دو خاصیت معروفی که در ویومدلها استفاده میشوند عبارتند از: DisplayName و Required. پشتیبانی از کلیدهای Resource به صورت توکار در خاصیت Required وجود دارد. برای استفاده از آنها باید به صورت زیر عمل کرد:
[Required(ErrorMessageResourceName = "ResourceKeyName", ErrorMessageResourceType = typeof(<SolutionName>.Resources.<ResourceClassName>))]
در کد بالا باید از نام فایل Resource اصلی (فایل اول که بدون نام کالچر بوده و به عنوان منبع پیشفرض به همراه یک فایل cs. حاوی کلاس مربوطه نیز هست) برای معرفی ErrorMessageResourceType استفاده کرد. چون ابزار توکار ویژوال استودیو از نام این فایل برای تولید کلاس مربوطه استفاده میکند.
متاسفانه خاصیت DisplayName که در فضای نام System.ComponentModel (در فایل System.dll) قرار دارد قابلیت استفاده از کلیدهای Resource را به صورت توکار ندارد. در دات نت 4 خاصیت دیگری در فضای نام System.ComponentModel.DataAnnotations به نام Display (در فایل System.ComponentModel.DataAnnotations.dll) وجود دارد که این امکان را به صورت توکار دارد. اما قابلیت استفاده از این خاصیت تنها در MVC 3 وجود دارد. برای نسخه‌های قدیمیتر MVC امکان استفاده از این خاصیت حتی اگر نسخه فریمورک هدف 4 باشد وجود ندارد، چون هسته این نسخه‌های قدیمی امکان استفاده از ویژگی‌های جدید فریمورک با نسخه بالاتر را ندارد. برای رفع این مشکل میتوان کلاس خاصیت DisplayName را برای استفاده از خاصیت Display به صورت زیر توسعه داد:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
  {
    private readonly DisplayAttribute _display;
    public LocalizationDisplayNameAttribute(string resourceName, Type resourceType)
    {
      _display = new DisplayAttribute { ResourceType = resourceType, Name = resourceName };
    }
    public override string DisplayName
    {
      get
      {
        try
        {
          return _display.GetName();
        }
        catch (Exception)
        {
          return _display.Name;
        }
      }
    }
  }
در این کلاس با ترکیب دو خاصیت نامبرده امکان استفاده از کلیدهای Resource فراهم شده است. در پیاده سازی این کلاس فرض شده است که نسخه فریمورک هدف حداقل برابر 4 است. اگر از نسخه‌های پایین‌تر استفاده میکنید در پیاده سازی این کلاس باید کاملا به صورت دستی کلید موردنظر را از Resource معرفی شده بدست آورید. مثلا به صورت زیر:
public class LocalizationDisplayNameAttribute : DisplayNameAttribute
{
    private readonly PropertyInfo nameProperty;
    public LocalizationDisplayNameAttribute(string displayNameKey, Type resourceType = null)
        : base(displayNameKey)
    {
        if (resourceType != null)
            nameProperty = resourceType.GetProperty(base.DisplayName, BindingFlags.Static | BindingFlags.Public);
    }
    public override string DisplayName
    {
        get
        {
            if (nameProperty == null) base.DisplayName;
            return (string)nameProperty.GetValue(nameProperty.DeclaringType, null);
        }
    }
}
برای استفاده از این خاصیت جدید میتوان به صورت زیر عمل کرد:
[LocalizationDisplayName("ResourceKeyName", typeof(<SolutionName>.Resources.<ResourceClassName>))]
البته بیشتر خواص متداول در ویومدلها از ویژگی موردبحث پشتیبانی میکنند.
نکته: به کار گیری این روش ممکن است در پروژه‌های بزرگ کمی گیج کننده و دردسرساز بوده و باعث پیچیدگی بی‌مورد کد و نیز افزایش بیش از حد حجم کدنویسی شود. در مقاله آقای فیل هک (Model Metadata and Validation Localization using Conventions) روش بهتر و تمیزتری برای مدیریت پیامهای این خاصیت‌ها آورده شده است.

پشتیبانی از ویژگی چند زبانه
مرحله بعدی برای چندزبانه کردن پروژه‌های MVC تغییراتی است که برای مدیریت Culture جاری برنامه باید پیاده شوند. برای اینکار باید خاصیت CurrentUICulture در ثرد جاری کنترل و مدیریت شود. یکی از مکانهایی که برای نگهداری زبان جاری استفاده میشود کوکی است. معمولا برای اینکار از کوکی‌های دارای تاریخ انقضای طولانی استفاده میشود. میتوان از تنظیمات موجود در فایل کانفیگ برای ذخیره زبان پیش فرض سیستم نیز استفاه کرد.
روشی که معمولا برای مدیریت زبان جاری میتوان از آن استفاده کرد پیاده سازی یک کلاس پایه برای تمام کنترلرها است. کد زیر راه حل نهایی را نشان میدهد:
public class BaseController : Controller
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    protected override void ExecuteCore()
    {
      var cookie = HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.ExecuteCore();
    }
  }
راه حل دیگر استفاده از یک ActionFilter است که نحوه پیاده سازی یک نمونه از آن در زیر آورده شده است:
public class LocalizationActionFilterAttribute : ActionFilterAttribute
  {
    private const string LanguageCookieName = "MyLanguageCookieName";
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      var cookie = filterContext.HttpContext.Request.Cookies[LanguageCookieName];
      string lang;
      if (cookie != null)
      {
        lang = cookie.Value;
      }
      else
      {
        lang = ConfigurationManager.AppSettings["DefaultCulture"] ?? "fa-IR";
        var httpCookie = new HttpCookie(LanguageCookieName, lang) { Expires = DateTime.Now.AddYears(1) };
        filterContext.HttpContext.Response.SetCookie(httpCookie);
      }
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang);
      base.OnActionExecuting(filterContext);
    }
  }
نکته مهم: تعیین زبان جاری (یعنی همان مقداردهی پراپرتی CurrentCulture ثرد جاری) در یک اکشن فیلتر بدرستی عمل نمیکند. برای بررسی بیشتر این مسئله ابتدا به تصویر زیر که ترتیب رخ‌دادن رویدادهای مهم در ASP.NET MVC را نشان میدهد دقت کنید:

همانطور که در تصویر فوق مشاهده میکنید رویداد OnActionExecuting که در یک اکشن فیلتر به کار میرود بعد از عملیات مدل بایندینگ رخ میدهد. بنابراین قبل از تعیین کالچر جاری، عملیات validation و یافتن متن خطاها از فایلهای Resource انجام میشود که منجر به انتخاب کلیدهای مربوط به کالچر پیشفرض سرور (و نه آنچه که کاربر تنظیم کرده) خواهد شد. بنابراین استفاده از یک اکشن فیلتر برای تعیین کالچر جاری مناسب نیست. راه حل مناسب استفاده از همان کنترلر پایه است، زیرا متد ExecuteCore قبل از تمامی این عملیات صدا زده میشود. بنابرابن همیشه کالچر تنظیم شده توسط کاربر به عنوان مقدار جاری آن در ثرد ثبت میشود.

امکان تعیین/تغییر زبان توسط کاربر
برای تعیین یا تغییر زبان جاری سیستم نیز روشهای گوناگونی وجود دارد. استفاده از زبان تنظیم شده در مرورگر کاربر، استفاده از عنوان زبان در آدرس صفحات درخواستی و یا تعیین زبان توسط کاربر در تنظیمات برنامه/سایت و ذخیره آن در کوکی یا دیتابیس و مواردی از این دست روشهایی است که معمولا برای تعیین زبان جاری از آن استفاده میشود. در کدهای نمونه ای که در بخشهای قبل آورده شده است فرض شده است که زبان جاری سیستم درون یک کوکی ذخیره میشود بنابراین برای استفاده از این روش میتوان از قطعه کدی مشابه زیر (مثلا در فایل Layout.cshtml_) برای تعیین و تغییر زبان استفاه کرد:
<select id="langs" onchange="languageChanged()">
  <option value="fa-IR">فارسی</option>
  <option value="en-US">انگلیسی</option>
</select>
<script type="text/javascript">
  function languageChanged() {
    setCookie("MyLanguageCookieName", $('#langs').val(), 365);
    window.location.reload();
  }
  document.ready = function () {
    $('#langs').val(getCookie("MyLanguageCookieName"));
  };
  function setCookie(name, value, exdays, path) {
    var exdate = new Date();
    exdate.setDate(exdate.getDate() + exdays);
    var newValue = escape(value) + ((exdays == null) ? "" : "; expires=" + exdate.toUTCString()) + ((path == null) ? "" : "; path=" + path) ;
    document.cookie = name + "=" + newValue;
  }
  function getCookie(name) {
    var i, x, y, cookies = document.cookie.split(";");
    for (i = 0; i < cookies.length; i++) {
      x = cookies[i].substr(0, cookies[i].indexOf("="));
      y = cookies[i].substr(cookies[i].indexOf("=") + 1);
      x = x.replace(/^\s+|\s+$/g, "");
      if (x == name) {
        return unescape(y);
      }
    }
  }
</script> 
متدهای setCookie و getCookie جاوا اسکریپتی در کد بالا از اینجا گرفته شده اند البته پس از کمی تغییر.
نکته: مطلب Cookieها بحثی نسبتا مفصل است که در جای خودش باید به صورت کامل آورده شود. اما در اینجا تنها به همین نکته اشاره کنم که عدم توجه به پراپرتی path کوکی‌ها در این مورد خاص برای خود من بسیار گیج‌کننده و دردسرساز بود. 
به عنوان راهی دیگر میتوان به جای روش ساده استفاده از کوکی، تنظیماتی در اختیار کاربر قرار داد تا بتواند زبان تنظیم شده را درون یک فایل یا دیتابیس ذخیره کرد البته با درنظر گرفتن مسائل مربوط به کش کردن این تنظیمات.
راه حل بعدی میتواند استفاده از تنظیمات مرورگر کاربر برای دریافت زبان جاری تنظیم شده است. مرورگرها تنظیمات مربوط به زبان را در قسمت Accept-Languages در HTTP Header درخواست ارسالی به سمت سرور قرار میدهند. بصورت زیر:
GET https://www.dntips.ir HTTP/1.1
...
Accept-Language: fa-IR,en-US;q=0.5
...
این هم تصویر مربوط به Fiddler آن:

نکته: پارامتر q در عبارت مشخص شده در تصویر فوق relative quality factor نام دارد و به نوعی مشخص کننده اولویت زبان مربوطه است. مقدار آن بین 0 و 1 است و مقدار پیش فرض آن 1 است. هرچه مقدار این پارامتر بیشتر باشد زبان مربوطه اولویت بالاتری دارد. مثلا عبارت زیر را درنظر بگیرید:
Accept-Language: fa-IR,fa;q=0.8,en-US;q=0.5,ar-BH;q=0.3
در این حالت اولویت زبان fa-IR برابر 1 و fa برابر 0.8 (fa;q=0.8) است. اولویت دیگر زبانهای تنظیم شده نیز همانطور که نشان داده شده است در مراتب بعدی قرار دارند. در تنظیم نمایش داده شده برای تغییر این تنظیمات در IE میتوان همانند تصویر زیر اقدام کرد:

در تصویر بالا زبان فارسی اولویت بالاتری نسبت به انگلیسی دارد. برای اینکه سیستم g11n دات نت به صورت خودکار از این مقادیر جهت زبان ثرد جاری استفاده کند میتوان از تنظیم زیر در فایل کانفیگ استفاده کرد:
<system.web>
    <globalization enableClientBasedCulture="true" uiCulture="auto" culture="auto"></globalization>
</system.web>
در سمت سرور نیز برای دریافت این مقادیر تنظیم شده در مرورگر کاربر میتوان از کدهای زیر استفاه کرد. مثلا در یک اکشن فیلتر:
var langs = filterContext.HttpContext.Request.UserLanguages;
پراپرتی UserLanguages از کلاس Request حاوی آرایه‌ای از استرینگ است. این آرایه درواقع از Split کردن مقدار Accept-Languages با کاراکتر ',' بدست می‌آید. بنابراین اعضای این آرایه رشته‌ای از نام زبان به همراه پارامتر q مربوطه خواهند بود (مثل "fa;q=0.8").
راه دیگر مدیریت زبانها استفاده از عنوان زبان در مسیر درخواستی صفحات است. مثلا آدرسی شبیه به www.MySite.com/fa/Employees نشان میدهد کاربر درخواست نسخه فارسی از صفحه Employees را دارد. نحوه استفاده از این عناوین و نیز موقعیت فیزیکی این عناوین در مسیر صفحات درخواستی کاملا به سلیقه برنامه نویس و یا کارفرما بستگی دارد. روش کلی بهره برداری از این روش در تمام موارد تقریبا یکسان است.
برای پیاده سازی این روش ابتدا باید یک route جدید در فایل Global.asax.cs اضافه کرد:
routes.MapRoute(
    "Localization", // Route name
    "{lang}/{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
دقت کنید که این route باید قبل از تمام routeهای دیگر ثبت شود. سپس باید کلاس پایه کنترلر را به صورت زیر پیاده سازی کرد:
public class BaseController : Controller
{
  protected override void ExecuteCore()
  {
    var lang = RouteData.Values["lang"];
    if (lang != null && !string.IsNullOrWhiteSpace(lang.ToString()))
    {
      Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(lang.ToString());
    }
    base.ExecuteCore();
  }
}
این کار را در یک اکشن فیلتر هم میتوان انجام داد اما با توجه به توضیحاتی که در قسمت قبل داده شد استفاده از اکشن فیلتر برای تعیین زبان جاری کار مناسبی نیست.
نکته: به دلیل آوردن عنوان زبان در مسیر درخواستها باید کتترل دقیقتری بر کلیه مسیرهای موجود داشت!

استفاده از ویوهای جداگانه برای زبانهای مختلف
برای اینکار ابتدا ساختار مناسبی را برای نگهداری از ویوهای مختلف خود درنظر بگیرید. مثلا میتوانید همانند نامگذاری فایلهای Resource از نام زبان یا کالچر به عنوان بخشی از نام فایلهای ویو استفاده کنید و تمام ویوها را در یک مسیر ذخیره کنید. همانند تصویر زیر:

البته اینکار ممکن است به مدیریت این فایلها را کمی مشکل کند چون به مرور زمان تعداد فایلهای ویو در یک فولدر زیاد خواهد شد. روش دیگری که برای نگهداری این ویوها میتوان به کار برد استفاده از فولدرهای جداگانه با عناوین زبانهای موردنظر است. مانند تصویر زیر:

روش دیگری که برای نگهداری و مدیریت بهتر ویوهای زبانهای مختلف از آن استفاده میشود به شکل زیر است:

استفاه از هرکدام از این روشها کاملا به سلیقه و راحتی مدیریت فایلها برای برنامه نویس بستگی دارد. درهر صورت پس از انتخاب یکی از این روشها باید اپلیکشن خود را طوری تنظیم کنیم که با توجه به زبان جاری سیستم، ویوی مربوطه را جهت نمایش انتخاب کند.
مثلا برای روش اول نامگذاری ویوها میتوان از روش دستکاری متد OnActionExecuted در کلاس پایه کنترلر استفاده کرد:
public class BaseController : Controller
{
  protected override void OnActionExecuted(ActionExecutedContext context)
  {
    var view = context.Result as ViewResultBase;
    if (view == null) return; // not a view
    var viewName = view.ViewName;
    view.ViewName = GetGlobalizationViewName(viewName, context);
    base.OnActionExecuted(context);
  }
  private static string GetGlobalizationViewName(string viewName, ControllerContext context)
  {
    var cultureName = Thread.CurrentThread.CurrentUICulture.Name;
    if (cultureName == "en-US") return viewName; // default culture
    if (string.IsNullOrEmpty(viewName))
      return context.RouteData.Values["action"] + "." + cultureName; // "Index.fa"
    int i;
    if ((i = viewName.IndexOf('.')) > 0) // ex: Index.cshtml
      return viewName.Substring(0, i + 1) + cultureName + viewName.Substring(i); // "Index.fa.cshtml"
    return viewName + "." + cultureName; // "Index" ==> "Index.fa"
  }
}
همانطور که قبلا نیز شرح داده شد، چون متد ExecuteCore قبل از OnActionExecuted صدا زده میشود بنابراین از تنظیم درست مقدار کالچر در ثرد جاری اطمینان داریم.
روش دیگری که برای مدیریت انتخاب ویوهای مناسب استفاده از یک ویوانجین شخصی سازی شده است. مثلا برای روش سوم نامگذاری ویوها میتوان از کد زیر استفاده کرد:
public sealed class RazorGlobalizationViewEngine : RazorViewEngine
  {
    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
      return base.CreatePartialView(controllerContext, GetGlobalizationViewPath(controllerContext, partialPath));
    }
    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
      return base.CreateView(controllerContext, GetGlobalizationViewPath(controllerContext, viewPath), masterPath);
    }
    private static string GetGlobalizationViewPath(ControllerContext controllerContext, string viewPath)
    {
      //var controllerName = controllerContext.RouteData.GetRequiredString("controller");
      var request = controllerContext.HttpContext.Request;
      var lang = request.Cookies["MyLanguageCookie"];
      if (lang != null && !string.IsNullOrEmpty(lang.Value) && lang.Value != "en-US")
      {
        var localizedViewPath = Regex.Replace(viewPath, "^~/Views/", string.Format("~/Views/Globalization/{0}/", lang.Value));
        if (File.Exists(request.MapPath(localizedViewPath))) viewPath = localizedViewPath;
      }
      return viewPath;
    }
و برای ثبت این ViewEngine در فایل Global.asax.cs خواهیم داشت:
protected void Application_Start()
{
  ViewEngines.Engines.Clear();
  ViewEngines.Engines.Add(new RazorGlobalizationViewEngine());
}

محتوای یک فایل Resource
ساختار یک فایل resx. به صورت XML استاندارد است. در زیر محتوای یک نمونه فایل Resource با پسوند resx. را مشاهده میکنید:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!-- 
    Microsoft ResX Schema ...
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   ...
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="RightToLeft" xml:space="preserve">
    <value>false</value>
    <comment>RightToleft is false in English!</comment>
  </data>
</root>
در قسمت ابتدایی تمام فایلهای resx. که توسط ویژوال استودیو تولید میشود کامنتی طولانی وجود دارد که به صورت خلاصه به شرح محتوا و ساختار یک فایل Resource میپردازد. در ادامه تگ نسبتا طولانی xsd:schema قرار دارد. از این قسمت برای معرفی ساختار داده ای فایلهای XML استفاده میشود. برای آشنایی بیشتر با XSD (یا XML Schema) به اینجا مراجعه کنید. به صورت خلاصه میتوان گفت که XSD برای تعیین ساختار داده‌ها یا تعیین نوع داده ای اطلاعات موجود در یک فایل XML به کار میرود. درواقع تگهای XSD به نوعی فایل XML ما را Strongly Typed میکند. با توجه به اطلاعات این قسمت، فایلهای resx. شامل 4 نوع گره اصلی هستند که عبارتند از: metadata و assembly و data و resheader. در تعریف هر یک از گره‌ها در این قسمت مشخصاتی چون نام زیر گره‌های قابل تعریف در هر گره و نام و نوع خاصیتهای هر یک معرفی شده است.
بخش موردنظر ما در این مطلب قسمت انتهایی این فایلهاست (تگهای resheader و data). همانطور در بالا مشاهده میکنید تگهای reheader شامل تنظیمات مربوط به فایل resx. با ساختاری ساده به صورت name/value است. یکی از این تنظیمات resmimetype فایل resource را معرفی میکند که درواقع مشخص کننده نوع محتوای (Content Type) فایل XML است(^). برای فایلهای resx این مقدار برابر text/microsoft-resx است. تنظیم بعدی نسخه مربوط به فایل resx (یا Microsoft ResX Schema) را نشان میدهد. در حال حاضر نسخه جاری (در VS 2010) برابر 2.0 است. تنظیم بعدی مربوط به کلاسهای reader و writer تعریف شده برای استفاده از این فایلهاست. به نوع این کلاسهای خواننده و نویسنده فایلهای resx. و مکان فیزیکی و فضای نام آنها دقت کنید که در مطالب بعدی از آنها برای ویرایش و بروزرسانی فایلهای resource در زمان اجرا استفاده خواهیم کرد.
در پایان نیز تگهای data که برای نگهداری داده‌ها از آنها استفاده میشود. هر گره data شامل یک خاصیت نام (name) و یک زیرگره مقدار (value) است. البته امکان تعیین یک کامنت در زیرگره comment نیز وجود دارد که اختیاری است. هر گره data مینواند شامل خاصیت type و یا mimetype نیز باشد. خاصیت type مشخص کننده نوعی است که تبدیل text/value را با استفاده از ساختار TypeConverter پشتیبانی میکند. البته اگر در نوع مشخص شده این پشتیبانی وجود نداشته باشد، داده موردنظر پس از سریالایز شدن با فرمت مشخص شده در خاصیت mimetype ذخیره میشود. این mimetype اطلاعات موردنیاز را برای کلاس خواننده این فایلها (ResXResourceReader به صورت پیشفرض) جهت چگونگی بازیابی آبجکت موردنظر فراهم میکند. مشخص کردن این دو خاصیت برای انواع رشته ای نیاز نیست. انواع mimetype قابل استفاده عبارتند از:
- application/x-microsoft.net.object.binary.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Binary.BinaryFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود (راجع به انکدینگ base64 ^ و ^).
- application/x-microsoft.net.object.soap.base64: آبجکت موردنظر باید با استفاده از کلاس System.Runtime.Serialization.Formatters.Soap.SoapFormatter سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
- application/x-microsoft.net.object.bytearray.base64: آبجکت ابتدا باید با استفاده از یک System.ComponentModel.TypeConverter به آرایه ای از بایت سریالایز شده و سپس با فرمت base64 به یک رشته انکد شود.
نکته: امکان جاسازی کردن (embed) فایلهای resx. در یک اسمبلی یا کامپایل مستقیم آن به یک سَتِلایت اسمبلی (ترجمه مناسبی برای satellite assembly پیدا نکردم، چیزی شبیه به اسمبلی قمری یا وابسته و از این قبیل ...) وجود ندارد. ابتدا باید این فایلهای resx. به فایلهای resources. تبدیل شوند. اینکار با استفاده از ابزار Resource File Generator (نام فایل اجرایی آن resgen.exe است) انجام میشود (^ و ^). سپس میتوان با استفاده از Assembly Linker ستلایت اسمبلی مربوطه را تولید کرد (^). کل این عملیات در ویژوال استودیو با استفاده از ابزار msbuild به صورت خودکار انجام میشود!

نحوه یافتن کلیدهای Resource در بین فایلهای مختلف Resx توسط پرووایدر پیش فرض در دات نت
عملیات ابتدا با بررسی خاصیت CurrentUICulture از ثرد جاری آغاز میشود. سپس با استفاده از عنوان استاندارد کالچر جاری، فایل مناسب Resource یافته میشود. در نهایت بهترین گزینه موجود برای کلید درخواستی از منابع موجود انتخاب میشود. مثلا اگر کالچر جاری fa-IR و کلید درخواستی از کلاس Texts باشد ابتدا جستجو برای یافتن فایل Texts.fa-IR.resx آغاز میشود و اگر فایل موردنظر یا کلید درخواستی در این فایل یافته نشد جستجو در فایل Texts.fa.resx ادامه می‌یابد. اگر باز هم یافته نشد درنهایت این عملیات جستجو در فایل resource اصلی خاتمه می‌یابد و مقدار کلید منبع پیش فرض به عنوان نتیجه برگشت داده میشود. یعنی در تمامی حالات سعی میشود تا دقیقترین و بهترین و نزدیکترین نتیجه انتخاب شود. البته درصورتیکه از یک پرووایدر شخصی سازی شده برای کار خود استفاده میکنید باید چنین الگوریتمی را جهت یافتن کلیدهای منابع خود از فایلهای Resource (یا هرمنبع دیگر مثل دیتابیس یا حتی یک وب سرویس) درنظر بگیرید.

Globalization در کلاینت (javascript g11n)
یکی دیگر از موارد استفاده g11n در برنامه نویسی سمت کلاینت است. با وجود استفاده گسترده از جاوا اسکریپت در برنامه نویسی سمت کلاینت در وب اپلیکیشنها، متاسفانه تا همین اواخر عملا ابزار یا کتابخانه مناسبی برای مدیریت g11n در این زمینه وجود نداشته است. یکی از اولین کتابخانه‌های تولید شده در این زمینه کتابخانه jQuery Globalization است که توسط مایکروسافت توسعه داده شده است (برای آشنایی بیشتر با این کتابخانه به ^ و ^ مراجعه کنید). این کتابخانه بعدا تغییر نام داده و اکنون با عنوان Globalize شناخته میشود. Globalize یک کتابخانه کاملا مستقل است که وابستگی به هیچ کتابخانه دیگر ندارد (یعنی برای استفاده از آن نیازی به jQuery نیست). این کتابخانه حاوی کالچرهای بسیاری است که عملیات مختلفی چون فرمت و parse انواع داده‌ها را نیز در سمت کلاینت مدیریت میکند. همچنین با فراهم کردن منابعی حاوی جفتهای key/culture میتوان از مزایایی مشابه مواردی که در این مطلب بحث شد در سمت کلاینت نیز بهره برد. نشانی این کتابخانه در github اینجا است. با اینکه خود این کتابخانه ابزار کاملی است اما در بین کالچرهای موجود در فایلهای آن متاسفانه پشتیبانی کاملی از زبان فارسی نشده است. ابزار دیگری که برای اینکار وجود دارد پلاگین jquery localize است که برای بحث g11n رشته‌ها پیاده‌سازی بهتر و کاملتری دارد.

در مطالب بعدی به مباحث تغییر مقادیر کلیدهای فایلهای resource در هنگام اجرا با استفاده از روش مستقیم تغییر محتوای فایلها و کامپایل دوباره توسط ابزار msbuild و نیز استفاده از یک ResourceProvider شخصی سازی شده به عنوان یک راه حل بهتر برای اینکار میپردازم.
در تهیه این مطلب از منابع زیر استفاده شده است:

اشتراک‌ها
AutoMapper 8.1.0 منتشر شد

AutoMapper 8.1 adds a major new feature - attribute-based maps. Attribute maps let you easily declare maps on destination types when you have straightforward scenarios. Instead of: 


public class OrderProfile {  
    public OrderProfile() {
        CreateMap<Order, OrderIndexModel>();
        CreateMap<Order, OrderEditModel>();
        CreateMap<Order, OrderCreateModel>();
    }
}


You can declare your type maps directly on the destination types themselves with AutoMapAttribute: 


[AutoMap(typeof(Order))]
public class OrderIndexModel {  
    // members
}

[AutoMap(typeof(Order))]
public class OrderEditModel {  
    // members
}

[AutoMap(typeof(Order))]
public class OrderCreateModel {  
    // members
}


AutoMapper 8.1.0 منتشر شد
نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت دوم - سرویس‌های پایه
اگر در داخل موجودیتون، همیشه به یکی از پراپرتی‌های موجود در Shadow properties نیاز دارید برای مثال به CreatedDateTime، همون پراپرتی (ها) رو در داخل موجودیت تعریف کنید و به راحتی در هر کوئری به اون دسترسی پیدا کنید.
public class Category : IAuditableEntity
{
    public int Id { get; set; }

    public Category()
    {
        Products = new HashSet<Product>();
    }
        
    public DateTime? CreatedDateTime { get; set; } //Here

    public string Name { get; set; }

    public string Title { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}
اما اگر فقط یکبار به اون نیاز دارید، از متود های GetShadowPropertyValue و نوع جنریک اون استفاده کنید. 
مطالب
ASP.NET MVC #4

بررسی نحوه ارتباطات بین اجزای مختلف الگوی MVC در ASP.NET MVC

اینبار برخلاف قسمت قبل، قالب پروژه خالی ASP.NET MVC را در VS.NET انتخاب کرده (ASP.NET MVC 3 Web Application و بعد انتخاب قالب Empty نمایش داده شده) و سپس پروژه جدیدی را شروع می‌کنیم. View Engine را هم Razor در نظر خواهیم گرفت.
پس از ایجاد ساختار اولیه پروژه، بدون اعمال هیچ تغییری، برنامه را اجرا کنید. بلافاصله با پیغام The resource cannot be found یا 404 یافت نشد، مواجه خواهیم شد.
همانطور که در پایان قسمت دوم نیز ذکر شد، پردازش‌ها در ASP.NET MVC از کنترلرها شروع می‌شوند و نه از صفحات وب. بنابراین برای رفع این مشکل نیاز است تا یک کلاس کنترلر جدید را اضافه کنیم. به همین جهت بر روی پوشه استاندارد Controllers کلیک راست کرده، از منوی ظاهر شده قسمت Add، گزینه‌ی Controller را انتخاب کنید:


در صفحه بعدی که ظاهر می‌شود، نام HomeController را وارد کنید (با توجه به اینکه مطابق قراردادهای ASP.NET MVC، نام کنترلر باید به کلمه Controller ختم شود). البته لازم به ذکر است که این مراحل را به همان شکل متداول مراجعه به منوی Project و انتخاب Add Class و سپس ارث بری از کلاس Controller نیز می‌توان انجام داد و طی این مراحل الزامی نیست. کلاسی که به صورت خودکار از طریق منوی Add Controller یاد شده ایجاد می‌شود، به شکل زیر است:

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

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

سؤال:
در قسمت دوم عنوان شد که کنترلر باید کلاسی باشد که اینترفیس IController را پیاده سازی کرده است، اما در اینجا ارث بری از کلاس Controller را شاهد هستیم. جریان چیست؟!
سلسله مراتبی که بکارگرفته شده به صورت زیر است:
public abstract class ControllerBase : IController
public abstract class Controller : ControllerBase

ControllerBase، اینترفیس IController را پیاده سازی کرده و سپس کلاس Controller از کلاس ControllerBase مشتق شده است. شاید بپرسید که این همه پیچ و تاب برای چیست؟!
مشکلی که با اینترفیس خالص وجود دارد، عدم نگارش پذیری آن است. به این معنا که اگر متدی یا خاصیتی در نگارش بعدی به این اینترفیس اضافه شد، هیچکدام از پروژه‌های قدیمی دیگر کامپایل نخواهند شد و باید ابتدا این متد یا خاصیت جدید را نیز لحاظ کنند. اینجا است که کار کلاس‌های abstract شروع می‌شود. در یک کلاس abstract می‌توان پیاده سازی پیش فرضی را نیز ارائه داد. به این ترتیب مصرف کننده نهایی کلاس Controller متوجه این تغییرات نخواهد شد.
اگر برنامه نویس «من» باشم، شما رو وادار خواهم کرد که متدهای جدید اینترفیس تعریفی‌ام را پیاده سازی کنید! همینه که هست! اما اگر طراح مایکروسافت باشد، بلافاصله انبوهی از جماعت ایرادگیر که بالای 100 تا از کنترلر‌های اون‌ها الان فقط در یک پروژه از کار افتاده،‌ ممکن است جلوی دفتر مایکروسافت دست به خود سوزی بزنند! اینجا است که مایکروسافت مجبور است تا این پیچ و تاب‌ها را اعمال کند که اگر روزی متدی در اینترفیس IController بنابر نیازهای جدید درنظر گرفته شد، بتوان سریع یک پیاده سازی پیش فرض از آن‌را در کلاس‌های abstract یاد شده قرار داد (یکی از تفاوت‌های مهم کلاس‌های abstract با اینترفیس‌ها) تا جماعت ایرادگیر و نق‌زن متوجه تغییری نشوند و باز هم پروژه‌های قدیمی بدون مشکل کامپایل شوند. تابحال به فلسفه وجودی کلاس‌های abstract از این دیدگاه فکر کرده بودید؟!

بعد از این توضیحات و کارها، اگر اینبار برنامه را اجرا کنیم، خطای زیر نمایش داده می‌شود:

The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Home/Index.aspx
~/Views/Home/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/Home/Index.cshtml
~/Views/Home/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

همانطور که ملاحظه می‌کنید چون نام کنترلر ما Home است، فریم ورک در پوشه استاندارد Views در زیر پوشه‌ای به همان نام Home، به دنبال یک سری فایل می‌گردد. فایل‌های aspx مربوط به View Engine ایی به همین نام بوده و فایل‌های cshtml و vbhtml مربوط به View Engine دیگری به نام Razor هستند.
بنابراین نیاز است تا یکی از این فایل‌ها را در مکان‌های یاد شده ایجاد کنیم. برای این منظور حداقل دو راه وجود دارد. یا دستی اینکار را انجام دهیم یا اینکه از ابزار توکار خود VS.NET برای ایجاد یک View جدید استفاده کنیم.
برای ایجاد View ایی مرتبط با متد Index (در ASP.NET MVC نام دیگر متدهای قرار گرفته در یک کنترلر، Action نیز می‌باشد)، روی خود متد کلیک راست کرده و گزینه Add View را انتخاب کنید:


در صفحه بعدی ظاهر شده، پیش فرض‌ها را پذیرفته و بر روی دکمه Add کلیک نمائید. اتفاقی که رخ خواهد داد شامل ایجاد فایل Index.cshtml، با محتوای زیر است (با توجه به اینکه زبان پروژه سی شارپ است و View Engine انتخابی Razor می‌باشد، cshtml تولید گردید و گرنه vbhtml ایجاد می‌شد):

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

مجددا برنامه را اجرا کنید. اینبار بدون خطایی کلمه Index را در صفحه تولیدی می‌توان مشاهده کرد. نکته جالب این فایل‌های View جدید، عدم مشاهده ویژگی‌های runat=server و سایر موارد مشابه است.

چند سؤال مهم:
در حین ایجاد اولین کنترلر جهت نمایش صفحه پیش فرض برنامه، نام HomeController انتخاب شد. چرا مثلا نام TestController وارد نشد؟ برنامه از کجا متوجه شد که باید حتما این کنترلر را پردازش کند. نقش متد Index چیست؟ آیا حتما باید Index باشد و در اینجا نام دیگری را نمی‌توان وارد کرد؟ «قرارداد» پردازشی این‌ها کجا تنظیم می‌شود؟ فریم ورک، این سیم کشی‌ها را چگونه انجام می‌دهد؟
پاسخ به تمام این سؤال‌ها، در ویژگی «مسیریابی یا Routing» نهفته است. فایل Global.asax.cs برنامه را باز کنید. تعاریف مرتبط با مسیریابی پیش فرض را در متد RegisterRoutes آن می‌توان مشاهده کرد:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}



پروسه هدایت یک درخواست HTTP به یک کنترلر، در اینجا مسیریابی یا Routing نامیده می‌شود. این قابلیت در فضای نام System.Web.Routing تعریف شده است و باید دقت داشت که جزو ASP.NET MVC نیست. این امکانات جزو ASP.NET Runtime است و به همراه دات نت 3.5 سرویس پک یک برای اولین بار ارائه شد. بنابراین جهت استفاده در ASP.NET Web forms نیز مهیا است. در ASP.NET MVC از این امکانات برای ارسال درخواست‌ها به کنترلرها استفاده می‌شود.
در متد routes.MapRoute فراخوانی شده‌ای که در کدهای بالا ملاحظه می‌کنید، کار نگاشت یک URL به Actionهای یک کنترلر صورت می‌گیرد (یا همان متدهای تعریف شده در کنترلرها). همچنین از این URLها پارامترهای این متدها یا اکشن‌ها نیز قابل استخراج است.
در متد MapRoute، اولین پارامتر تعریف شده، یک نام پیش فرض است و در ادامه اگر آدرسی را به فرم «یک چیزی اسلش یک چیزی اسلش یک چیزی» یافت، اولین قسمت آن‌را به عنوان نام کنترلر تفسیر خواهد کرد، دومین قسمت آن، نام متد عمومی موجود در کنترلر فرض شده و سومین قسمت به عنوان پارامتر ارسالی به این متد پردازش می‌شود.
برای مثال از آدرس زیر اینطور می‌توان دریافت که:
http://hostname/home/about

Home نام کنترلی است که فریم ورک به دنبال آن خواهد گشت تا این درخواست رسیده را پردازش کند و about نام متدی عمومی در این کلاس است که به صورت خودکار فراخوانی می‌گردد. در اینجا پارامتر id ایی هم وجود ندارد. در یک برنامه امکان تعریف چندین و چند مسیریابی وجود دارد.
پارامتر سوم متد routes.MapRoute، یک سری پیش فرض را تعریف می‌کند و این مورد همانجایی است که از اطلاعات آن جهت تعریف کنترلر پیش فرض استفاده کردیم. برای مثال به چهار آدرس زیر دقت نمائید:

http://localhost/
http://localhost/home
http://localhost/home/about
http://localhost/process/list

در حالت http://localhost/،‌ هر سه مقدار پیش فرض مورد استفاده قرار خواهند گرفت چون سه جزئی ({controller}/{action}/{id}) که موتور مسیریابی به دنبال آن‌ها می‌گردد، در این آدرس وجود خارجی ندارد. بنابراین نام کنترلر پیش فرض در این حالت همان Home مشخص شده در پارامتر سوم متد routes.MapRoute خواهد بود و متد پیش فرض نیز Index و پارامتری هم به آن ارسال نخواهد شد.
در حالت http://localhost/home نام کنترلر مشخص است اما دو جزء دیگر ذکر نشده‌اند، بنابراین مقادیر آن‌ها از پیش فرض‌های صریح ذکر شده در متد routes.MapRoute گرفته می‌شود. یعنی نام متد اکشن مورد پردازش، Index خواهد بود به همراه هیچ آرگومان خاصی.
به علاوه باید خاطرنشان کرد که این مقادیر case sensitive یا حساس به بزرگی و کوچکی حروف نیستند. بنابراین مهم نیست که http://localhost/HoMe باشد یا http://localhost/HOMe یا هر ترکیب دیگری.
یا اگر آدرس رسیده http://localhost/process/list باشد، این مسیریابی پیش فرض تعریف شده قادر به پردازش آن می‌باشد. به این معنا که کنترلری به نام Process وهله سازی و سپس متدی به نام List در آن فراخوانی خواهند شد.

یک نکته:
ترتیب routes.MapRouteهای تعریف شده در اینجا مهم است و اگر اولین URL رسیده با الگوی تعریف شده مطابقت داشت، کار را تمام خواهد کرد و به سایر تعاریف نخواهد رسید. مثلا اگر در اینجا یک مسیریابی دیگر را به نام Process تعریف کنیم:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Process", // Route name
"Process/{action}/{id}", // URL with parameters
new { controller = "Process", action = "List", id = UrlParameter.Optional } // Parameter defaults
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

آدرسی به فرم http://localhost/Process، به صورت خودکار به کنترلر Process و متد عمومی List آن نگاشت خواهد شد و کار به مسیریابی بعدی نخواهد رسید.