Lee Louviere
Lee Louviere

Reputation: 5262

ModelState binding custom array of checkboxes

ViewModel Binding is working, the object passed back to the edit controller contains the correct values, which is a list of selected options. However, ModelState binding is not working, the model state AttemptedValues exist, but aren't being reloaded into the fields.

I have a model with the following properties

class Model
{
    public List<string> AvailableValues { get; set; }
    public List<string> SelectedValues { get; set; }
}

But in my view I have some categorization, so I can't do a direct foreach.

foreach (var category in CatgoryList.Categories)
{
    foreach (var available in Model.AvailableValues.Where(x => category.AvailableValues.Contains(x))
    {
        var check = Model.SelectedValues!= null && Model.SelectedValues.Contains(available.Id);
        check &= (ViewData.ModelState["SelectedValues"] != null) && ViewData.ModelState["SelectedValues"].Value.AttemptedValue.Contains(available.Id);
        <input type="checkbox" name="SelectedValues" id="available.Id" value="available.Id" checked="@check"/>@available.FriendlyName<br>
    }
}

The ModelState does contain SelectedValues from the previous post, but it doesn't auto-bind, because I have a custom field for the checkboxes.

This code is smelly

Is there a better way to get the data to load from the Attempted Value

EDIT:

Ok, so my question wasn't clear enough, let me clarify.

On a validate, I'm retuning the same view if there was an error. The modelstate is holding the previously entered values in ModelState["field"].Value.AttemptedValue. With fields created using the helpers, TextboxFor, CheckboxFor, etc, these values are automatically filled in.

However, when using the normal reflexes for checkbox binding, only the values of the checked checkboxes are returned in the data object passed back to the controller. This means I'm not using the logic that fills values in from the ModelState.

What I've done is dig through the modelstate myself for the attempted values, because they do exist under the field name "SelectedValues". But I have to manually apply them. The value there looks like this.

ModelState["SelectedValues"] = "Value1;Value2;Value4"

Is there a better way to get the data to load from the Attempted Value in the model state.

Upvotes: -1

Views: 1062

Answers (2)

Lee Louviere
Lee Louviere

Reputation: 5262

Ok, so basically, I couldn't find anything that will do this for me. The default Html helper methods just don't cover this scenario.

So, I wrote an extension method.

Basically it pulls in the enumerator from the model using the expression you send to it, just like any other helper, but you also send the entry in the list you want to build a checkbox against.

It ends up looking like this.

@Html.CheckboxListEntryFor(x => x.SelectedEntries, AvailableEntries[i].Id)

The method does the following

  1. Get the propertyInfo for the list and check if selected entries contains the values.
  2. Check if the ModelState is invalid, if so, overwrite the checked value with the modelstate entry
  3. build an html checkbox that uses the property name as the name and id of the checkbox, and sets checked based on the previous steps.

    public static MvcHtmlString CheckboxListEntryFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression, string entryValue)
    {
        PropertyInfo info = GetPropertyInfo(typeof (TModel), expression);
        var enumerator = info.GetValue(htmlHelper.ViewData.Model);
        var check = enumerator != null && ((IList) enumerator).Contains(entryValue);
        if (!htmlHelper.ViewData.ModelState.IsValid)
        {
            check = htmlHelper.ViewData.ModelState[info.Name] != null &&
                htmlHelper.ViewData.ModelState[info.Name].Value.AttemptedValue.Contains(entryValue);
        }
    
        var fieldString = String.Format(
                          "<input type=\"checkbox\" name=\"{0}\" id =\"{1}\" value=\"{1}\"{2}/>",
                          info.Name, entryValue, check ? " checked=\"checked\"" : string.Empty);
    
        return MvcHtmlString.Create(fieldString);
    }
    

Upvotes: 0

Greg Ennis
Greg Ennis

Reputation: 15379

The primary "smell" (to use your term) I see here is that the code you have in the nested foreach is written directly in your view (*.cshtml), but code of that complexity should be in your Controller action.

You should calculate and generate all the data your view will need in the controller, and then pass that data through to the view using Model (looks like you are already doing that) and you can also use the ViewBag to pass additional data not contained in your Model. Then the view is just responsible to generate the HTML.

That's the other problem I see with your code - you are referencing the ViewData.ModelState which is highly unusual to see in a view. The ModelState should be examined in the controller before you even decide which view to render.

It looks like maybe you are just passing data through ViewData.ModelState that should actually be passed through ViewData/ViewBag.

You can read more about passing data to a view here.

Upvotes: 1

Related Questions