Search code examples
c#asp.net-mvcpostaction-filter

How to change form values after post in ASP.NET MVC?


I want to change form value before pass it to action of controller. but it throws Collection is read-only.

public class PersonController : Controller
{
    public ActionResult Add()
    {
        return View();
    }

    [HttpPost]
    [PersianDateConvertor("birthday")]
    public ActionResult Add(FormCollection collection)
    {
        string firstName = collection["firstName"];
        string lastName = collection["lastName"];
        string birthday = collection["birthday"];

        return View();
    }
}
public class PersianDateConvertorAttribute : ActionFilterAttribute
{
    string[] fields;
    public PersianDateConvertorAttribute(params string[] persianDateFieldNames)
    {
        if (persianDateFieldNames == null)
            fields = new string[] { };
        else
            fields = persianDateFieldNames;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        foreach (var field in fields)
        {
            string value = filterContext.HttpContext.Request.Form[field];
            filterContext.HttpContext.Request.Form.Remove(field); //throws Collection is read-only
            filterContext.HttpContext.Request.Form.Add(field, ConvertToGregorian(value));
            // or filterContext.HttpContext.Request.Form[field] = ConvertToGregorian(value);
        }

        base.OnActionExecuting(filterContext);
    }
}

Solution

  • If I understand correctly, you want to modify the behaviour of the DateTime during the binding process. Instead of using an attribute, I would use a ModelBinder to change the format of the date string.

    I did something similar for a problem while converting decimal values from multiple cultures: (the code is taken from a blog post, it's not mine, but I don't remember the source. Sorry)

    using System;
    using System.Globalization;
    using System.Web.Mvc;
    
    public class DecimalModelBinder : IModelBinder
    {
      public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
      {
        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        ModelState modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
          actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
        }
        catch (FormatException e)
        {
          modelState.Errors.Add(e);
        }
        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
      }
    }
    

    in global.asax you register the binder

    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
      RegisterGlobalFilters(GlobalFilters.Filters);
      RegisterRoutes(RouteTable.Routes);
    
      ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
    
    }
    

    Imo this is a better approach and you don't have to put an attribute to every action