Search This Blog

Sunday, September 7, 2014

ASP.NET MVC Custom Model Binder - Custom DateTime Model binder - Apply Model Binder with Attributes

  • Custom Datetime Model Binder - 1 


Let’s say that you have a model with a DateTime property.

public class Person
{
    //...
    public DateTime DateOfBirth { get; set; }
}
 
Suppose that you want someone to fill out the details and post them back to your site. Simple enough: add a couple of action methods and then insert an EditorFor in your view:
 
//controller
public ActionResult EditDetails()
{
    return View(new Person()); //or loaded from DB, etc
}
 
[HttpPost]
public ActionResult SaveDetails(Person person)
{
    //save updated person to DB
}


@Html.EditorFor(model => model.DateOfBirth)
That’s all you need for basic date editing, though for your users’ sake I suggest you look at a nicer editor than the plain input that this will generate! But what if you want to support some format besides the default? MVC doesn’t handle that out-of-the-box so you’ll need a custom model binder…

Custom Model Binder 

 The model binder is responsible for converting a series of form values into the rich, strongly-typed model that is passed as a parameter to your action method.

 Thankfully there’s no need to re-implement all of the pretty-complex functionality involved in that process just to alter how dates are parsed; a custom model binder can be associated with a single type, and the rest of the binding functionality will work as normal.

Custom model binder implementations need to implement the IModelBinder interface, but the easier approach is to inherit from System.Web.Mvc.DefaultModelBinder and override the one method in which we are interested: BindModel
 
public class DateTimeModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // ???
    }
}

The BindModel method is responsible for returning the value of a model based on the controller- and the binding-context (which between them provide a lot of contextual information about the request), and our custom date parsing implementation will need to:
  • get hold of the custom format that we want to use 
  • use that format with the value that has been posted back to the server 

Specifying the Date Format 

There are a lot of different ways in which you might determine or specify the custom format, but for the purposes of this example I am just going to pass it into the constructor.
 

public class DateTimeModelBinder : DefaultModelBinder
{
    private string _customFormat;
 
    public DateTimeModelBinder(string customFormat)
    {
        _customFormat = customFormat;
    }
 
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        // ???
    }
}

In a real implementation you might grab this from a data annotation attribute, from configuration, from the request information – wherever makes sense for your project.

 Getting the POSTed Value 

The bindingContext that is passed to the BindModel method gives us a couple of properties that we can use to get the POSTed value from the request:
  •  ModelName gives us the name of the property on which the binder is being used 
  • ValueProvider.GetValue(string) will return an instance of ValueProviderResult from which we can get the raw value 

The ValueProvider.GetValue method takes a “key” as the parameter, for which we can use the value of the ModelName property:
 
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
value.AttemptedValue; //provides the raw value
From here it is a simple step to tie everything together with a DateTime.ParseExact:
 
public class DateTimeModelBinder : DefaultModelBinder
{
    private string _customFormat;
 
    public DateTimeModelBinder(string customFormat)
    {
        _customFormat = customFormat;
    }
 
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        return DateTime.ParseExact(value.AttemptedValue, _customFormat, CultureInfo.InvariantCulture);
    }
}

Hooking it up 

 Now that we have the model binder, we need to associate it with all DateTime properties throughout our application. We do this by adding an instance of the binder to the ModelBinders.Binders collection from within Global.asax.cs.
 
protected void Application_Start()
{
    //...
    var binder = new DateTimeModelBinder(GetCustomDateFormat());
    ModelBinders.Binders.Add(typeof(DateTime), binder);
    ModelBinders.Binders.Add(typeof(DateTime?), binder);
}

This will associate our new binder with any properties or values of type DateTime or DateTime?. Now, whenever one of these types is encountered by MVC whilst trying to parse a POSTed value it will use our custom binder and therefore our custom format!

//=====================================================================


  • Custom Datetime Model Binder - 2 --- with attribute


In my view I am going to provision three fields for the date within my form. In my model I will only have one property for the three fields called a Date:
 

Day Month Year
public class HomePageModels { public string Title { get; set; } public string Date { get; set; } }

Custom Binding 

 The custom binding class needs to inherit form IModelBinder. Here we capture the current request and extract the Form fields individually. Then we can manipulate these fields any way we like. In this example as you can see I am adding them to a single property called Date.

Alternatively if we do not want to implement custom binding for each and every Model and Property in our application we can inherit from the DefaultModelBinder and override the BindModel method as below
 

public class HomeCustomBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, 
                            ModelBindingContext bindingContext)
    {
        HttpRequestBase request = controllerContext.HttpContext.Request;

        string title = request.Form.Get("Title");
        string day = request.Form.Get("Day");
        string month = request.Form.Get("Month");
        string year = request.Form.Get("Year");

        return new HomePageModels
                   {
                       Title = title,
                       Date = day +"/"+ month +"/"+ year
                   };
    }
} 

public class HomeCustomDataBinder : DefaultModelBinder
    {

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType == typeof(HomePageModels))
            {
                HttpRequestBase request = controllerContext.HttpContext.Request;

                string title = request.Form.Get("Title");
                string day = request.Form.Get("Day");
                string month = request.Form.Get("Month");
                string year = request.Form.Get("Year");

                return new HomePageModels
                {
                    Title = title,
                    Date = day + "/" + month + "/" + year
                };

                //// call the default model binder this new binding context
                //return base.BindModel(controllerContext, newBindingContext);
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }

    } 

Once we have completed coding our custom class we will need to register the class which I do in the Global.asax under Application_Start().
 
protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    AuthConfig.RegisterAuth();
    ModelBinders.Binders.Add(typeof(HomePageModels), new HomeCustomBinder());
}

Controller 

Finally we need to inform the controller as to the binding we want it to use. This we can specify using attributes [ModelBinder(typeof(HomeCustomBinder))] as below:
 

[HttpPost]
public ActionResult Index([ModelBinder(typeof(HomeCustomBinder))] HomePageModels home)
{
    if (ModelState.IsValid)
    {
        ViewBag.Title = home.Title;
        ViewBag.Date = home.Date;
    }
    return View();
}

1 comment:

Contact Form

Name

Email *

Message *