یکدست کردن «ی» و «ک» در ASP.NET Core با پیاده‌سازی یک Model Binder سفارشی
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

معادل مطلب جاری را برای ASP.NET MVC 5.x در مطلب «یکدست کردن "ی" و "ک" در ASP.NET MVC با پیاده‌سازی یک Model Binder» می‌توانید مطالعه کنید. در اینجا قصد داریم یک چنین قابلیتی را با توجه به تغییرات ASP.NET Core نیز تهیه کنیم.


تهیه یک binder provider پردازش رشته‌ها

کار model binding، تطابق اطلاعات رسیده‌ی از درخواست جاری، با پارامترهای اکشن متد یک کنترلر است. هر مقدار رسیده، به یک binder متناسب ارسال می‌شود تا پردازش آن مدیریت گردد. به صورت پیش فرض در ASP.NET Core، تعدد 14 عدد binder providers که اینترفیس IModelBinderProvider را پیاده سازی می‌کنند، در این بین جهت یافتن یک binder مناسب، بررسی خواهند شد. برای مثال کار یک binder، پردازش نوع‌های پیچیده‌است (complex types) و دیگری نوع‌های ساده (simple types) مانند int و string را پردازش می‌کند.


public class CustomStringModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }
 
        if (context.Metadata.IsComplexType)
        {
            return null;
        }
 
        var fallbackBinder = new SimpleTypeModelBinder(context.Metadata.ModelType);
        if (context.Metadata.ModelType == typeof(string))
        {
            return new CustomStringModelBinder(fallbackBinder);
        }
        return fallbackBinder;
    }
}
بنابراین اولین قدم تهیه‌ی یک model binder سفارشی، تهیه‌ی یک تامین کننده‌ی سفارشی است که با پیاده سازی اینترفیس IModelBinderProvider ارائه می‌شود. در اینجا چون می‌خواهیم نوع‌های ساده‌ی رشته‌ای را پردازش کنیم، اگر نوع جاری رسیده، یک نوع پیچیده بود (context.Metadata.IsComplexType) نال را بازگشت می‌دهیم تا model binder بعدی ثبت شده‌ی در لیست تامین کننده‌های مرتبط، مورد آزمایش قرار گیرد.
سپس اگر نوع مدل جاری رشته‌ای بود، وهله‌ای از CustomStringModelBinder را بازگشت می‌دهیم (کلاسی است که آن‌را در ادامه تهیه خواهیم کرد). درغیراینصورت همان SimpleTypeModelBinder توکار این فریم‌ورک را بازگشت خواهیم داد.


تهیه‌ی یک model binder سفارشی پردازش رشته‌ها

تا اینجا تامین کننده‌ای را که مشخص می‌کند چه model binder ایی قرار است بازگشت داده شود، تهیه کردیم. مرحله‌ی بعد، پیاده سازی CustomStringModelBinder با پیاده سازی اینترفیس IModelBinder است:
public class CustomStringModelBinder : IModelBinder
{
    private readonly IModelBinder _fallbackBinder;
    public CustomStringModelBinder(IModelBinder fallbackBinder)
    {
        if (fallbackBinder == null)
        {
            throw new ArgumentNullException(nameof(fallbackBinder));
        }
        _fallbackBinder = fallbackBinder;
    }
 
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }
 
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult != ValueProviderResult.None)
        {
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
 
            var valueAsString = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(valueAsString))
            {
                return _fallbackBinder.BindModelAsync(bindingContext);
            }
 
            var model = valueAsString.Replace((char)1610, (char)1740).Replace((char)1603, (char)1705);
            bindingContext.Result = ModelBindingResult.Success(model);
            return Task.CompletedTask;
        }
 
        return _fallbackBinder.BindModelAsync(bindingContext);
    }
}
عملیات اصلی پردازشی یک Model binder در متد BindModelAsync آن صورت می‌گیرد. ابتدا مقداری را که در حال پردازش است دریافت می‌کنیم (توسط ValueProvider.GetValue). سپس ی و ک آن‌را یکدست کرده و به عنوان نتیجه‌ی عملیات تنظیم خواهیم کرد. این کار سبب خواهد شد تا هر مقداری را که کاربر وارد و ارسال کند، پیش از رسیدن به اکشن متد و پارامترهای آن، مورد پردازش و یکدست سازی قرار گیرد.
در اینجا تمام مواردی را که نمی‌خواهیم پردازش کنیم، به همان SimpleTypeModelBinder که از طریق سازنده‌ی کلاس دریافت می‌کنیم، واگذار خواهیم کرد.


معرفی به binder provider سفارشی به سیستم

مرحله‌ی آخر این عملیات، معرفی binder تهیه شده به سیستم است که روش آن را در ذیل مشاهده می‌کنید:
public static class CustomStringModelBinderExtensions
{
    public static MvcOptions UseCustomStringModelBinder(this MvcOptions options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }
 
        var simpleTypeModelBinder = options.ModelBinderProviders.FirstOrDefault(x => x.GetType() == typeof(SimpleTypeModelBinderProvider));
        if (simpleTypeModelBinder == null)
        {
            return options;
        }
 
        var simpleTypeModelBinderIndex = options.ModelBinderProviders.IndexOf(simpleTypeModelBinder);
        options.ModelBinderProviders.Insert(simpleTypeModelBinderIndex, new CustomStringModelBinderProvider());
        return options;
    }
}
در اینجا ابتدا به دنبال SimpleTypeModelBinderProvider توکار گشته و سپس آن‌را با CustomStringModelBinderProvider خود جایگزین می‌کنیم. اگر این model binder سفارشی ما در ایندکس نامناسبی در لیست options.ModelBinderProviders قرارگیرد، هیچگاه فراخوانی نخواهد شد؛ برای مثال اگر پس از SimpleTypeModelBinderProvider قرارگیرد.
در آخر تنها کافی است در کلاس آغازین برنامه، متد الحاقی UseCustomStringModelBinder فوق را به تنظیمات Mvc اضافه کنیم:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.UseCustomStringModelBinder(); 
    });
  • #
    ‫۶ سال و ۵ ماه قبل، شنبه ۱۱ فروردین ۱۳۹۷، ساعت ۱۸:۲۶
    یک نکته‌ی تکمیلی در مورد context.Metadata.IsComplexType

    اگر اکشن متد شما یک چنین امضایی را دارد:
    public IActionResult Index([FromBody] MyViewModel model)
    {
    هیچگاه model binder تعریف شده سفارشی، بر روی خواص MyViewModel اعمال نخواهد شد؛ از این جهت که ویژگی FromBody، کار پردازش Request Body را مستقلا انجام داده و پس از یکبار پردازش، پردازش مجددی بر روی آن صورت نخواهد گرفت (بدنه‌ی درخواست، یک non-rewindable stream است). به همین جهت دیگر کار به فراخوانی یک Model Binder سفارشی نمی‌رسد. بنابراین اگر می‌خواهید Model Binder سفارشی شما بر روی خواص یک شیء نیز اعمال شود، باید ویژگی FromBody را حذف کنید.
    البته با حذف FromBody، اطلاعات از یکی از سه منبع زیر خوانده خواهند شد:
    - form-URL-encoded body
    contentType: 'application/x-www-form-urlencoded; charset=utf-8'
    - route values
    - query string
    • #
      ‫۵ سال قبل، جمعه ۸ شهریور ۱۳۹۸، ساعت ۱۷:۰۷
      با حذف قسمت [FromBody] از امضای اکشن متد زیر : 
      public IActionResult Create([FromBody] CreateViewModel vm)
      {
      باز هم نال برگردوند مدل رو از سمت ویو وقتی این Annotation رو قرار میدم بالای viewmodel.
      این هم ViewModel بنده :
      [ModelBinder(BinderType = typeof(PersianDateModelBinder))]
      public partial class CreateViewModel
       {
              public int Id { get; set; }
              [DisplayName("عنوان مطلب")]
              public string Title { get; set; }
              [DisplayName("نگارش مطلب")]
              public string Content { get; set; }
              [DisplayName("تاریخ درج")]
              public DateTime CreateDate { get; set; } = DateTime.Now.Date;
              [DisplayName("تاریخ انتشار")]
              public DateTime PublishDate { get; set; } = DateTime.Now.Date;
              
       }
      این دو تا تاریخ رو هم (CreateDate و PublishDate) هم در همان viewModel مقدار دهی اولیه کردم (البته نمیدونم این کار اینجا درست هستش یا نه) که اگه کاربر از داخل PersianDatePicker انتخاب نکرد تاریخ امروز رو بفرسته.
        • #
          ‫۵ سال قبل، جمعه ۸ شهریور ۱۳۹۸، ساعت ۱۷:۳۴
          اگه منظورتون این هستش :
          <form asp-action="Create" Method="post" enctype="application/x-www-form-urlencoded">
          که اعمال شده.(البته تا اونجا که یادمه اگه این enctype رو نزاریم هم پیش فرض Post همین هستش ولی من خودم قرار دادم)

  • #
    ‫۶ سال و ۵ ماه قبل، یکشنبه ۱۲ فروردین ۱۳۹۷، ساعت ۱۵:۴۲
    یک نکته‌ی تکمیلی: غیر سراسری تعریف کردن یک Model Binder سفارشی

    روش استفاده‌ی از options.ModelBinderProviders.Insert، یک Model Binder را به صورت سراسری به کل برنامه اعمال می‌کند. اگر می‌خواهید این Binder فقط به یک ViewModel خاص اعمال شود، می‌توان به صورت زیر عمل کرد (بدون نیازی به Insert آن در options.ModelBinderProviders):
    [ModelBinder(BinderType = typeof(PersianDateModelBinder))]
    public class MyViewModel
    {
     البته در این حالت Binder تعریف شده نباید دارای پارامتری در سازنده‌ی آن باشد.
  • #
    ‫۱ سال و ۱۰ ماه قبل، چهارشنبه ۲۰ مهر ۱۴۰۱، ساعت ۱۴:۱۹
    برای پیاده سازی این روش در Blazor، هم از مقاله شما استفاده کردم و هم از روش این مقاله، اما متاسفانه نشد. راه حلی که خودم به نظرم رسید پالایش مدل دریافت شده از کامپوننت در لایه سرویس برنامه توسط یک متد مثل زیر است:
            public static string funConvertToStandard(string inputString)
            {            
                //تبدیل اعداد فارسی به انگلیسی جهت ذخیره سازی یکنواخت در بانک
                inputString = inputString.Replace("٠", "0");
                inputString = inputString.Replace("١", "1");
                inputString = inputString.Replace("٢", "2");
                inputString = inputString.Replace("٣", "3");
                inputString = inputString.Replace("۴", "4");
                inputString = inputString.Replace("۵", "5");
                inputString = inputString.Replace("۶", "6");
                inputString = inputString.Replace("٧", "7");
                inputString = inputString.Replace("٨", "8");
                inputString = inputString.Replace("٩", "9");
                //تبدیل ی و ک عربی به‌ی و ک فارسی
                inputString = inputString.Replace((char)1609, (char)1740);
                inputString = inputString.Replace((char)1610, (char)1740);
                inputString = inputString.Replace((char)1603, (char)1705);
                //یکنواخت سازی نیم فاصله          
                //inputString = inputString.Replace("‏", "‌");//تبدیل نیم فاصله 8207 به نیم فاصله 8204          
                return inputString.Trim();
            }
    اما خوب روش مقاله شما خیلی جامع‌تر است اگر قابلیت استفاده در Blazor را داشته باشد.