Search code examples
asp.net-mvc-3model-bindingdefaultmodelbindercustom-model-binder

How do I get MVC Model Binder to only bind non Null values for a list?


Following this question I was wondering if there is anyway to make MVC model binder only bind elements to a list if there is a value to populate them. For example if have a form with three inputs with the same name and one value isn't entered how do I stop MVC binding a list that has 3 elements one of which is null?


Solution

  • Custom Model Binder

    You could implement your own model binder to prevent the null values from being added to the list:

    View:

    @model MvcApplication10.Models.IndexModel
    
    <h2>Index</h2>
    
    @using (Html.BeginForm())
    {
        <ul>
            <li>Name: @Html.EditorFor(m => m.Name[0])</li>
            <li>Name: @Html.EditorFor(m => m.Name[1])</li>
            <li>Name: @Html.EditorFor(m => m.Name[2])</li>
        </ul>
    
        <input type="submit" value="submit" />
    }
    

    Controller:

    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            return View();
        }
    
        [HttpPost]
        public ActionResult Index(IndexModel myIndex)
        {
            if (ModelState.IsValid)
            {
                return RedirectToAction("NextPage");
            }
            else
            {
                return View();
            }
        }
    }
    

    Model:

    public class IndexModel
    {
        public List<string> Name { get; set; }
    }
    

    Custom Model Binder:

    public class IndexModelBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            bool hasPrefix = bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName);
            string searchPrefix = (hasPrefix) ? bindingContext.ModelName + "." : "";
    
            List<string> valueList = new List<string>();
    
            int index = 0;
            string value;
    
            do
            {
                value = GetValue(bindingContext, searchPrefix, "Name[" + index + "]");
    
                if (!string.IsNullOrEmpty(value))
                {
                    valueList.Add(value);
                }
    
                index++;
    
            } while (value != null); //a null value indicates that the Name[index] field does not exist where as a "" value indicates that no value was provided.
    
            if (valueList.Count > 0)
            {
                //obtain the model object. Note: If UpdateModel() method was called the model will have been passed via the binding context, otherwise create our own.
                IndexModel model = (IndexModel)bindingContext.Model ?? new IndexModel();
                model.Name = valueList;
                return model;
            }
    
            //No model to return as no values were provided.
            return null;
        }
    
        private string GetValue(ModelBindingContext context, string prefix, string key)
        {
            ValueProviderResult vpr = context.ValueProvider.GetValue(prefix + key);
            return vpr == null ? null : vpr.AttemptedValue;
        }
    }
    

    You will need to register the model binder in the Application_Start() (global.asax):

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
    
            //this will use your custom model binder any time you add the IndexModel to an action or use the UpdateModel() method.
            ModelBinders.Binders.Add(typeof(IndexModel), new IndexModelBinder());
    
            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);
        }
    

    Custom Validation Attribute

    Alternatively, you could validate that all of the values are populated by using a custom attribute:

    View:

    @model MvcApplication3.Models.IndexModel
    
    <h2>Index</h2>
    
    @using (Html.BeginForm())
    {
        @Html.ValidationMessageFor(m => m.Name)
        <ul>
            <li>Name: @Html.EditorFor(m => m.Name[0])</li>
            <li>Name: @Html.EditorFor(m => m.Name[1])</li>
            <li>Name: @Html.EditorFor(m => m.Name[2])</li>
        </ul>
    
        <input type="submit" value="submit" />
    }
    

    Controller:

    Use the same controller defined above.

    Model:

    public class IndexModel
    {
        [AllRequired(ErrorMessage="Please enter all required names")]
        public List<string> Name { get; set; }
    }
    

    Custom Attribute:

    public class AllRequiredAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            bool nullFound = false;
    
            if (value != null && value is List<string>)
            {
                List<string> list = (List<string>)value;
    
                int index = 0;
    
                while (index < list.Count && !nullFound)
                {
                    if (string.IsNullOrEmpty(list[index]))
                    {
                        nullFound = true;
                    }
                    index++;
                }
            }
    
            return !nullFound;
        }
    }