WebGrid
pagination links work properly in all cases, except one (that I noticed).
When you use CheckBoxFor
in MVC, it creates an input[type=hidden]
and an input[type=check-box]
for the same field so that it can handle state. So, if you have a field named X
and submit your form in the GET
method, you will end up with an URL like this:
http://foo.com?X=false&X=true
The default model binder can understand these multiple instances os X
and figure out it's value.
The problem occurs when you try to paginate the WebGrid
. It's behavior is to try catch the current request parameters and repass them in the pagination links. However, as there's more than one X
, it will pass X=false,true
instead of the expected X=false
or X=false&X=true
That's a problem because X=false,true
won't bind properly. It will trigger an exception in the model binder, prior to the beggining of the action.
Is there a way I can solve it?
EDIT:
It seems like a very specific problem but it's not. Nearly every search form with a check box will break the WebGrid pagination. (If you are using GET)
EDIT 2:
I think my only 2 options are:
- Build my own WebGrid pager that is more clever on passing parameters for pagination links
- Build my own Boolean model binder that understands
false,true
as valid
In case anybody else is suffering from the issue described, you can work around this using a custom model binder like this:
public class WebgridCheckboxWorkaroundModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType == typeof (Boolean))
{
var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
if (value.AttemptedValue == "true,false")
{
PropertyInfo prop = bindingContext.Model.GetType().GetProperty(propertyDescriptor.Name, BindingFlags.Public | BindingFlags.Instance);
if (null != prop && prop.CanWrite)
{
prop.SetValue(bindingContext.Model, true, null);
}
return;
}
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
An alternative to inheriting from the DefaultModelBinder would be to implement the IModelBinder interface specifically for nullable boolean values.
internal class BooleanModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// Get the raw attempted value from the value provider using the ModelName
var param = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (param != null)
{
var value = param.AttemptedValue;
return value == "true,false";
}
return false;
}
}
You can then add the model binding to the Global.asax or add it to the parameter like so:
public ActionResult GridRequests([ModelBinder(typeof(BooleanModelBinder))]bool? IsShowDenied, GridSortOptions sort, int? page)
{
...
}
For those who would like to implement the custom model binder in Dan's solution, but aren't sure how.
You have to register the model binder in the Global.asax file:
protected void Application_Start()
{
ModelBinders.Binders.Add(typeof(HomeViewModel), new WebgridCheckboxWorkaroundModelBinder());
}
And also specify its use in your action:
public ActionResult Index([ModelBinder(typeof(WebgridCheckboxWorkaroundModelBinder))] HomeViewModel viewModel)
{
//code here
return View(viewModel);
}