Select All checkbox on EditorFor MVC C# View

2019-07-25 08:03发布

问题:

My current view is displaying a list of Employees. All rows and columns displayed are model bound.

See my view code below:

@using System.Linq
@using DN_.Extensions
@model DN_.Models.NotificationsModel
<script src="~/Scripts/jquery-3.3.1.min.js"></script>

<script type="text/javascript" language="javascript">
    $(function () {
        $("#checkAll").click(function () {
            $("input[id='cb_Notify']").prop("checked", this.checked).change();
            var count = $("input[name='cb_Notify']:checked").length;
        })

        $("input[id='cb_Notify']").click(function () {
            if ($("input[name='cb_Notify']:checked").length == $("input[id='cb_Notify']").length) {
                $("#checkAll").prop("checked", "checked").change();
            }
            else {
                $("#checkAll").removeProp("checked").change();
            }
        })
    })
</script>

@{
    ViewBag.Title = "Link Employees";
}

<h2>Link Employees</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    <input id="btn_Save" type="submit" value="Save" class="btn btn-default" />

    @Html.ActionLink("Back to List", "Index")
    <p>
        Select All <input type="checkbox" id="checkAll" />
        Select All On Premise <input type="checkbox" id="checkAllOnPremise" />
        Select All Full Timers<input type="checkbox" id="checkAllFullTimers" />
    </p>
    <table class="table">
        <tr>
            <th align=center>Notify?</th>
            <th align=center>Employee Name</th>
            <th align=center>Is On Premise</th>
            <th align=center>Is Full Time</th>
            <th align=center>Notified On</th>
        </tr>

        @for (var i = 0; i < Model.EmployeeNotification.Count; i++)
        {

            <tr>
                <td>
                    @*Do not allow editing of the Notify field for employees who have been sent the notification already*@
                    @if (Model.EmployeeNotification[i].NotifiedOn >= DateTime.Parse("2000-01-01 12:00:00 AM"))
                    {
                        @Html.DisplayFor(modelItem => Model.EmployeeNotification[i].Notify)
                    }
                    else
                    {
                        @*Hidden items for the post back information*@
                        @Html.HiddenFor(modelItem => Model.EmployeeNotification[i].NotificationID)
                        @Html.HiddenFor(modelItem => Model.EmployeeNotification[i].EmployeeID)
                        @Html.HiddenFor(modelItem => Model.EmployeeNotification[i].EmployeeName)

                        @*BELOW 3 LINES ARE THE PROBLEM DESCRIBED*@
                        @Html.EditorFor(modelItem => Model.EmployeeNotification[i].Notify)
                        @Html.CheckBoxFor(modelItem => Model.EmployeeNotification[i].Notify)
                        @*This checkbox below works with the "Select All" option, but data is not posted back.*@
                        @Html.CheckBox("cb_Notify", Model.EmployeeNotification[i].Notify)
                    }
                </td>
                <td>
                    @Model.EmployeeNotification[i].EmployeeName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => Model.EmployeeNotification[i].IsOnPremise)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => Model.EmployeeNotification[i].IsFullTime)
                </td>
                <td>
                    @if (Model.EmployeeNotification[i].NotifiedOn >= DateTime.Parse("2000-01-01 12:00:00 AM"))
                    {
                        @Html.RenderDate(Model.EmployeeNotification[i].NotifiedOn, "dd MMM yyyy")
                    }
                </td>
            </tr>
        }
    </table>
}

My problem is as follows: I can manually select all Notify checkboxes using the first Notify Checkbox code line (i.e. using the EditorFor and CheckBoxFor option) and save the data in the post back event. How do I get the select All checkbox option to work on the EditorFor or CheckBoxFor model bound checkbox.

For me the named CheckBox option works as intended with the Select All box, but I fail to be able to get the data back to the post event handler. The model data for the selected Notify column is returned as null.

The main problem I presume is in the way the automation names the elements for the checkboxes if I look at the generated name and id element codes when debugging (sample output source code data with some of my comments added):

<td>
    <!--Required, hidden data for post back event handler:-->
    <input name="EmployeeNotification[3].NotificationID" id="EmployeeNotification_3__NotificationID" type="hidden" value="6" data-val-number="The field NotificationID must be a number." data-val="true">
    <input name="EmployeeNotification[3].EmployeeID" id="EmployeeNotification_3__EmployeeID" type="hidden" value="27" data-val-number="The field EmployeeID must be a number." data-val="true">
    <input name="EmployeeNotification[3].EmployeeName" id="EmployeeNotification_3__EmployeeName" type="hidden" value="Charlie">
    <!--@Html.EditorFor element-->
    <input name="EmployeeNotification[3].Notify" id="EmployeeNotification_3__Notify" type="checkbox" value="true" data-val="true" data-val-required="The Notify field is required.">
    <input name="EmployeeNotification[3].Notify" type="hidden" value="false">
    <!--@Html.CheckBoxFor element-->
    <input name="EmployeeNotification[3].Notify" id="EmployeeNotification_3__Notify" type="checkbox" value="true">
    <input name="EmployeeNotification[3].Notify" type="hidden" value="false">
    <!--@Html.CheckBox element-->
    <input name="cb_Notify" id="cb_Notify" type="checkbox" value="true">
    <input name="cb_Notify" type="hidden" value="false">
</td>

So I either need to get the Select All checkbox to work with the @Html.EditorFor or @Html.CheckBoxFor options OR I need to obtain the value set in the @Html.CheckBox in the post event handler.

I have not been able to get the answers I need in the other, similar questions on the Select or Check All Checkboxes as they appear to use other coding languages. Please help.

Please note:

Just to put the obvious out there: The intention is to only retain 1 of the selectable Notify checkboxes in the end. The one that will work. Not all three of them. I just have them displayed for now due to my unsuccessful testing and debugging.

回答1:

Because your name (and id) attributes include the collection indexer, you could use a jQuery Attribute Ends With Selector (for example $('input[type="checkbox"][name$="Notify"]')), but this would be easier just using a class name for the checkboxes.

@Html.CheckBoxFor(m => m.EmployeeNotification[i].Notify, new { @class = "notify" })

and then you scripts can be

// cache for performance
var checkboxes = $('.notify');
var total = checkboxes.length;
var checkall = $('#checkAll');

checkall.change(function() {
    checkboxes.prop('checked', $(this).is(':checked'));
})

checkboxes.change(function() {
    var count = checkboxes.filter(':checked').length;
  checkall.prop('checked', (count == total));
})

However you have another issue with your code. By default, the DefaultModelBinder required collection indexers to be zero-based and consecutive. If the first item in your collection meets the @if (Model.EmployeeNotification[i].NotifiedOn >= DateTime.Parse("2000-01-01 12:00:00 AM")) condition, then your EmployeeNotification property will be will be null in the POST method. ALternatively, if say the 3rd item meets that condition, then EmployeeNotification will only contain the first 2 records. You need to add an additional input for the collection indexer to allow the DefaultModelBinder to bind non-zero/non-consecutive indexers

@if (Model.EmployeeNotification[i].NotifiedOn >= DateTime.Parse("2000-01-01 12:00:00 AM"))
{
    @Html.DisplayFor(m => m.EmployeeNotification[i].Notify)
}
else
{
    @Html.HiddenFor(m => m.EmployeeNotification[i].NotificationID)
    @Html.HiddenFor(m => m.EmployeeNotification[i].EmployeeID)
    @Html.HiddenFor(m => m.EmployeeNotification[i].EmployeeName)
    @Html.CheckBoxFor(m => m.EmployeeNotification[i].Notify, new { @class = "notify" })
    <input type="hidden" name="EmployeeNotification.Index" value="@i" /> // add this
}

In addition, I recommend you delete the other hidden inputs except the one that is the ID proeprty (which I assume is NotificationID). It is just including unnecessary html, since those properties should not be required for saving the data, and it is just allowing malicious users to alter those values. I also recommend your view model contains a (say) bool IsEditable property and you set that value based on the condition in the GET method when you map the data model to the view model, so that the if block becomes just @if (Model.EmployeeNotification[i].IsEditable) { ... } else { ... }



回答2:

I managed to also find another solution to Stephen's post above using some inspiration from his suggestions. Not as clean, but hey, it works. I definitely recommend Stephen's code. Much cleaner and concise.

  1. Move and hide the custom cb_Notify checkbox to the top of the cell so it gets created with every row. This will make it searchable etc for all rows and I can use it in a loop:

    @Html.CheckBox("cb_Notify", Model.EmployeeNotification[i].Notify, new { type = "hidden" })
    @*Do not allow editing of the Notify field for employees who have been sent the notification already*@
    @if (Model.EmployeeNotification[i].NotifiedOn >= DateTime.Parse("2000-01-01 12:00:00 AM"))
    {
        @Html.DisplayFor(modelItem => Model.EmployeeNotification[i].Notify)
    }
    else
    {
        @*Hidden items for the post back information*@
        @Html.HiddenFor(modelItem => Model.EmployeeNotification[i].NotificationID)
        @Html.HiddenFor(modelItem => Model.EmployeeNotification[i].EmployeeID)
        @Html.CheckBoxFor(modelItem => Model.EmployeeNotification[i].Notify, new { @class = "notify" })
    }
    
  2. Then for the jquery, use the hidden checkbox to loop through all rows and set the values that need to be set:

    // cache for performance
    var checkAll = $('#checkAll');
    
    checkAll.click(function () {
        var maxCount = $("input[id='cb_Notify']").length;
        var loopCounter;
        var customer;
        for (loopCounter = 0; loopCounter < maxCount; loopCounter++) {
            customer = $("input[name='EmployeeNotification[" + loopCounter + "].EmployeeName']").val();
            if (customer != null) {
                $("input[name='EmployeeNotification[" + loopCounter + "].Notify']").prop("checked", this.checked);
            }
        }
        toggleSetAllCheckBoxStates();
    })
    

The toggleSetAllCheckBoxStates() function I created due to having the secondary requirement (out of scope to this question so I'll leave it out) to select subset only of the Employee data based on its data settings. But it follows along the same lines as the above sample.

This way on the post back event I filtered out the null data as the ones that have been sent notifications for would not contain the EmployeeID. The rest of the data is saved successfully.