2

I have a form that needs to capture values from checkboxes in a form. Each checkbox should have an integer value and when the form is submitted, the view model should validate the values and at least one should be selected.

I need to also build a two way binding so that the framework will auto select the options that are selected when the page is loaded.

Here is what my model looks like

public class SomeViewModel 
{
    [Required(ErrorMessage = "You must select at least one site.")]
    [Display(Name = "All site to have access too")]
    public int[] Sites { get; set; }
}

The I encapsulate my ViewModel in a Presentation class called Presenter like so

public class Presenter
{
    public SomeViewModel Access { get; set; }

    public IEnumerable<Site> AvailableSites { get; set; }
}

Now, I pass Presenter to my view and want to render the

And here is how my view looks like

<div class="form-group">

    @Html.LabelFor(m => m.Access.Sites, new { @class = "col-sm-2 control-label" })

    <div class="col-sm-10">

        @for (int i = 0; i < Model.AvailableSites.Count(); i++ )
        {
            <label class="radio-inline">
                @Html.CheckBoxFor(m => m.Access.Sites[i]) 
            </label>
        }

    </div>

    @Html.ValidationMessageFor(m => m.Access.Sites)
</div>

since @Html.CheckBoxFor accepts bool value, and I am passing an integer I am getting an error on the @Html.CheckBoxFor(m => m.Access.Sites[i]) line inside the view.

How can I correct this issue? and how can I correctly render checkboxes in this view?

Jaylen
  • 39,043
  • 40
  • 128
  • 221
  • Use Editor templates. Take a look at [How to know the selected checkboxes from within the HttpPost Create action method?](http://stackoverflow.com/questions/38961222/how-to-know-the-selected-checkboxes-from-within-the-httppost-create-action-metho) – Shyju Sep 24 '16 at 18:35
  • are you saying I can't do two way binding and I have to manually check for a selected value? – Jaylen Sep 24 '16 at 18:56

1 Answers1

3

As you have discovered, you can only use CheckBoxFor() to bind to a bool property. Its a bit unclear why you have 2 view models for this when you could simplify it by just using

public class Presenter
{
    [Required(ErrorMessage = "You must select at least one site.")]
    [Display(Name = "All site to have access too")]
    public int[] Sites { get; set; 
    public IEnumerable<Site> AvailableSites { get; set; }
}

One option you can consider (based on the above model and assuming type of Site contains properties int ID and string Name)

Change the view to to manually generate <input type="checkbox" /> elements

@foreach(var site in Model.AvailableSites)
{
    // assumes your using Razor v2 or higher
    bool isSelected = Model.Sites.Contains(s.ID);
    <label>
        <input type="checkbox" name="Sites" value="@site.ID" checked = @isSelected />
        <span>@s.Name</span>
    </label>
}
@Html.ValidationMessageFor(m => m.Sites)

Note the @Html.LabelFor(m => m.Sites) is not appropriate. A <label> is an accessibility element for setting focus to its associated form control and you do not have a form control for Sites. You can use <div>@Html.DisplayNameFor(m => m.Sites)</div>

A better option (and the MVC way) is to create a view model representing what you want in the view

public class SiteVM
{
    public int ID { get; set; }
    public string Name { get; set; }
    public bool IsSelected { get; set; }
}

and in your GET method, return a List<SiteVM> based on all available site (for example

List<SiteVM> model = db.Sites.Select(x => new SiteVM() { ID = x.ID, Name = x.Name };
return View(model);

and in the view use a for loop of EditorTemplate (refer this answer for more detail) to generate the view

@model List<SiteVM>
....
@using (Html.BeginForm())
{
    for(int i = 0; i < Model.Count; i++)
    {
        @Html.HiddenFor(m => m[i].ID)
        @Html.HiddenFor(m => m[i].Name)
        <label>
            @Html.CheckBoxFor(m => m[i].IsSelected)
            <span>@Model[i].Name</span>
        </label>
        @Html.ValidationMessage("Sites")
    }
    ....
}

and then in the POST method

[HttpPost]
public ActionResult Edit(List<SiteVM> model)
{
    if (!model.Any(x => x.IsSelected))
    {
        ModelState.AddModelError("Sites", "Please select at least one site");
    }
    if (!ModelState.IsValid)
    {
        return View(model);
    }
    ...
}

In either case you cannot get client side validation using jquery.validate.unobtrusive because you do not (and cannot) create a form control for a property which is a collection. You can however write your own script to display the validation message and cancel the default submit, for example (assumes you use @Html.ValidationMessageFor(m => m.Sites, "", new { id = "sitevalidation" }))

var sitevalidation = $('#sitevalidation');
$('form').submit(function() {
    var isValid = $('input[type="checkbox"]:checked').length > 0;
    if (!isValid) {
        sitevalidation.text('Please select at least one site');
        return false; // cancel the submit
    }
}
$('input[type="checkbox"]').click(function() {
    var isValid = $('input[type="checkbox"]:checked').length > 0;
    if (isValid) {
        sitevalidation.text(''); // remove validation message
    }
}
Community
  • 1
  • 1