اگر چند سالی باشه که با ASP.NET MVC کار می‌کنید، احتمالاً قبول دارید که یکی از لذت‌بخش‌ترین بخش‌های کار با این فریم‌ورک، «سادگی» در عین «قدرت» اون هست. وقتی در یک اکشن کنترلر، یک مدل پیچیده رو به عنوان ورودی دریافت می‌کنید، همه‌چیز خیلی جادویی و راحت به نظر می‌رسه.

اما این جادو از کجا میاد؟ چطور می‌شه که MVC به صورت خودکار می‌فهمه که داده‌های ارسال شده از یک فرم HTML یا یک درخواست API رو باید دقیقاً در کدام پراپرتی‌های کلاس مدل شما قرار بده؟

جواب در یک کلمه است: ModelBinder.

تو این مقاله، می‌خوایم پرده از این جادوی پنهان برداریم. می‌خوایم ببینیم ModelBinder دقیقاً چیه، چطور کار می‌کنه و مهم‌تر از همه، چطور می‌تونیم نسخه «سفارشی» خودمون رو بنویسیم تا سناریوهای پیچیده‌تر رو مدیریت کنیم.

پس تا انتهای این مقاله همراه من باشید.

ModelBinder چیست و چرا اهمیت دارد؟

به زبان ساده، ModelBinder پلی است بین دنیای بی‌ساختار (String-based) درخواست‌های HTTP و دنیای ساختاریافته (Strongly-typed) کدهای C# شما.

وقتی کاربری فرمی رو پر می‌کنه و دکمه «ارسال» رو می‌زنه، مرورگر تمام اون داده‌ها رو به صورت یک سری جفت‌های کلید-مقدار (Key-Value) متنی به سمت سرور می‌فرسته. (مثل Name=Hossein و Price=15000).

حالا، شما در اکشن کنترلرتون همچین کدی نوشتید:

public IActionResult Create(ProductViewModel model)
{
    // ...
}

ModelBinder موجودی هست که بین درخواست HTTP و این اکشن می‌شینه. وظیفه‌اش اینه که اون داده‌های متنی Name و Price رو بخونه، تشخیص بده که باید به پراپرتی‌های Name و Price در کلاس ProductViewModel تبدیل بشن، اون‌ها رو تبدیل (Cast) کنه و در نهایت یک آبجکت ProductViewModel تر و تمیز و آماده رو تحویل اکشن شما بده.

تصویر از مدل بایندر و ارتباط اون با تبدیل اطلاعات ارسالی با درخواست به مدل

Model binding در ASP.NET Core MVC داده‌ها را از درخواست‌های HTTP (مقادیر فرم، داده‌های مسیر، پارامترهای query string، هدرهای HTTP) به پارامترهای اکشن کنترلر و پراپرتی‌های مدل مپ می‌کند.

بدون ModelBinder، شما مجبور بودید در تک‌تک اکشن‌هاتون به صورت دستی Request.Form["Name"] رو بخونید، اون رو به نوع داده مورد نظر تبدیل کنید، اعتبارسنجی کنید و... . یک کار تکراری و پر از خطا.

جادوی پشت پرده: DefaultModelBinder

در ۹۹ درصد مواقع، شما اصلاً نیازی به انجام کار خاصی ندارید. ASP.NET MVC به صورت پیش‌فرض از یک کلاس به نام DefaultModelBinder استفاده می‌کنه. این کلاس به قدری هوشمند هست که می‌تونه اکثر سناریوهای رایج رو مدیریت کنه.

DefaultModelBinder به ترتیب در منابع زیر دنبال داده می‌گرده:

  1. داده‌های فرم (Form Values): داده‌هایی که از طریق یک فرم POST می‌شوند.
  2. داده‌های مسیر (Route Values): پارامترهایی که در URL شما تعریف شدن (مثلاً {id} در products/edit/5).
  3. داده‌های Query String: پارامترهایی که بعد از علامت ? در URL میان (مثلاً ?page=2).

بیایید یک مثال ساده رو ببینیم.

فرض کنید این مدل رو داریم:

// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

و این اکشن کنترلر:

// Controllers/ProductsController.cs
public class ProductsController : Controller
{
    [HttpPost]
    public IActionResult Create(Product product)
    {
        if (!ModelState.IsValid)
        {
            return View(product);
        }

        // At this point, 'product' object is fully populated.
        // For example: product.Name might be "لپ تاپ توسینسو"

        // ... save product to database ...

        return RedirectToAction("Index");
    }
}

وقتی فرمی با فیلدهای Name و Price به این اکشن POST می‌شه، DefaultModelBinder به طور خودکار آبجکت product رو برای شما پر می‌کنه. به همین سادگی.

چرا و چه زمانی به Model Binder سفارشی (Custom) نیاز داریم؟

همونطور که گفتم، DefaultModelBinder در ۹۹ درصد مواقع عالیه. اما اون ۱ درصد باقی‌مونده چی؟

گاهی اوقات شما داده‌هایی رو دریافت می‌کنید که در یک فرمت استاندارد نیستن و DefaultModelBinder نمی‌فهمه چطور باید اون‌ها رو به مدل شما تبدیل کنه.

یک سناریوی واقعی: فرض کنید شما در حال ساخت یک سیستم مدیریت تگ (Tag) هستید. کاربر می‌تونه در یک فیلد متنی، تگ‌ها رو با ویرگول (comma) جدا کنه و وارد کنه. مثلاً: "asp.net,mvc,c#".

در سرور، شما نمی‌خواید این رو به صورت یک رشته (string) دریافت کنید. شما می‌خواید مستقیماً اون رو به صورت یک لیست از رشته‌ها (List<string>) یا حتی یک آرایه (string[]) تحویل بگیرید.

// Your model
public class BlogPost
{
    public string Title { get; set; }
    public string Body { get; set; }
    public string[] Tags { get; set; } // We want to bind "asp.net,mvc" to this
}

// Your action
public IActionResult Create(BlogPost post)
{
    // DefaultModelBinder will NOT know how to convert
    // the string "asp.net,mvc" from the form
    // into the string[] Tags property.
    // 'post.Tags' will probably be null or empty.
}

DefaultModelBinder وقتی به پراپرتی Tags می‌رسه، گیج می‌شه. اون انتظار داره داده‌ای به اسم Tags[0]Tags[1] و... دریافت کنه، نه یک رشته ساده که با ویرگول جدا شده.

اینجا دقیقاً همون نقطه‌ای هست که باید آستین‌ها رو بالا بزنید و Model Binder سفارشی خودتون رو بنویسید.

گام به گام: ساخت یک Model Binder سفارشی

ساختن یک Model Binder سفارشی دو مرحله اصلی داره: ۱. ساخت کلاس Binder که اینترفیس IModelBinder رو پیاده‌سازی می‌کنه. ۲. معرفی (Register) کردن این Binder به فریم‌ورک MVC.

بیایید همون سناریوی تگ‌ها رو پیاده‌سازی کنیم.

مرحله ۱: ساخت کلاس CustomModelBinder

ما یک کلاس می‌سازیم (مثلاً CsvToCollectionModelBinder) که اینترفیس IModelBinder رو پیاده‌سازی می‌کنه. این اینترفیس فقط یک متد اصلی داره به نام BindModelAsync.

// Binders/CsvToCollectionModelBinder.cs
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Threading.Tasks;

public class CsvToCollectionModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingC null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        // 1. Get the name of the property we are trying to bind.
        string modelName = bindingContext.ModelName;

        // 2. Try to get the value from the request (Form, QueryString, etc.)
        ValueProviderResult valueProviderResult = 
            bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            // No value was provided for this property.
            return Task.CompletedTask;
        }

        // 3. Set the state that we found a value.
        bindingContext.ModelState.SetModelValue(modelName, 
            valueProviderResult);

        // 4. Get the actual string value.
        string value = valueProviderResult.FirstValue;

        if (string.IsNullOrEmpty(value))
        {
            // Value is empty, nothing to split.
            return Task.CompletedTask;
        }

        // 5. This is our custom logic!
        // Split the comma-separated string into an array.
        var stringArray = value.Split(',')
                               .Select(s => s.Trim())
                               .ToArray();

        // 6. Set the successful result.
        // The binder will set this array to the property (e.g., Tags).
        bindingContext.Result = ModelBindingResult.Success(stringArray);

        return Task.CompletedTask;
    }
}

مرحله ۲: معرفی (Register) کردن Binder

حالا که Binder رو ساختیم، باید به MVC بگیم که «هر وقت خواستی پراپرتی Tags رو در مدل BlogPost بایند کنی، از این کلاس استفاده کن».

این کار به سادگی با استفاده از یک Attribute به نام [ModelBinder] انجام می‌شه.

مدل BlogPost رو به این شکل به‌روزرسانی می‌کنیم:

// Updated Models/BlogPost.cs
using Microsoft.AspNetCore.Mvc;

public class BlogPost
{
    public string Title { get; set; }
    public string Body { get; set; }

    [ModelBinder(BinderType = typeof(CsvToCollectionModelBinder))]
    public string[] Tags { get; set; }
}

تمام!

از این به بعد، هر وقت اکشنی BlogPost رو به عنوان ورودی بگیره، فریم‌ورک MVC می‌بینه که پراپرتی Tags یک ModelBinder سفارشی داره. به جای استفاده از DefaultModelBinder، میره سراغ کلاس CsvToCollectionModelBinder ما. کلاس ما هم اون رشته‌ی "asp.net,mvc" رو می‌گیره، اون رو به آرایه‌ی ["asp.net", "mvc"] تبدیل می‌کنه و تحویل مدل post می‌ده.

حالا در اکشن کنترلر، post.Tags یک آرایه آماده و قابل استفاده است.

نتیجه‌گیری

Model Binding یکی از قدرتمندترین و در عین حال نادیده‌گرفته‌شده‌ترین قابلیت‌های ASP.NET MVC هست. درک عمیق این مکانیزم به شما کمک می‌کنه تا کنترل کاملی روی نحوه ترجمه درخواست‌های ورودی به آبجکت‌های C# داشته باشید.

DefaultModelBinder رفیق خوب شما در ۹۹ درصد مواقع هست، اما برای اون ۱ درصد سناریوهای خاص و پیچیده، نترسید و Custom Model Binder خودتون رو بنویسید. این کار کدهای شما رو به شدت تمیزتر، خواناتر و حرفه‌ای‌تر می‌کنه.