2

Scenario: I have a table with rows that are populated from a ViewModel. There are checkboxes for each row that allow the user to check 1 or more of the rows and then choose from actions in a dropdown menu to make edits to properties on the selected rows.

Everything works fine to this point, and I can get the ViewModel to pass correctly and then use it and all it's properties in a POST Action method. I could make the changes based on the option the user picked.

However, since some of the options in the dropdown would make fairly substantial and irreversible changes, I am calling a new View with a GET and populating a new table with just the selected rows, and asking the user to confirm they want to make the changes. Everything is still good up to this point. The new View populates as expected with only the rows that were selected in the previous View.

Problem: After the user confirms their intent, an Action method is called with POST. The ViewModel that correctly populated the current View is making its way into the controller correctly. I get the ViewModel, but not with the same properties as the one that populated the View.

ViewModel

public class ProjectIndexViewModel
{        
    public List<ProjectDetailsViewModel> Projects { get; set; }
    public string FlagFormEditProjects { get; set; }
    public string FlagFormNewProjectStatus { get; set; }
}

The List<ProjectDetailsViewModel> Projects is what is used to populate the rows in the table, and Projects are what are not binding correctly in the POST Action methods in the controller.

Initial View where the checkboxes are selected. Note the example of one of the javascript functions that is called when one of the dropdown options is selected, which is what submits the form.

@using (Html.BeginForm("EditProjectsTable", "Project", FormMethod.Get, new { name = "formEditProjects", id = "formEditProjects" }))
{         
    @Html.HiddenFor(item => item.FlagFormEditProjects)
    @Html.HiddenFor(item => item.FlagFormNewProjectStatus) 
    ....
    <table>
        <thead>
            ....
        </thead>
        <tbody>
            @for (int i = 0; i < Model.Projects.Count; i++)
            {
                <tr>
                    <td>@Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
                    <td>@Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
                    .... // more display properties
                    <td>
                        @Html.CheckBoxFor(x => x.Projects[i].Selected, new { @class = "big-checkbox" })
                        @Html.HiddenFor(x => x.Projects[i].ProjectModelId)
                    </td>
                </tr>
            }                         
        </tbody>
    </table>
}

function submitFormRemoveProjects() {
    $("#FlagFormEditProjects").attr({
        "value": "RemoveProjects"
    });
    $('#formEditProjects').submit();
}

Action method that returns the "confirmation" View (works fine)

[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable([Bind(Include = "Projects,FlagFormEditProjects,FlagformNewProjectStatus")]ProjectIndexViewModel projectIndexViewModel)
{
    // Repopulate the Projects collection of ProjectIndexViewModel to
    // include only those that have been selected
    return View(projectIndexViewModel);
}

View that is returned from Action method above (works fine) Note that the Action method that gets called is set dynamically with the actionName variable in the Html.BeginForm call.

@using (Html.BeginForm(actionName, "Project", FormMethod.Post))
{
    @Html.AntiForgeryToken()
    @Html.HiddenFor(model => model.FlagFormNewProjectStatus)
    ....
    <table>
        <thead>
            ....
        </thead>
        <tbody>
            @for (int i = 0; i < Model.Projects.Count; i++)
            {
                <tr>
                    <td>@Html.HiddenFor(x => x.Projects[i].ProjectModelId)</td>
                    <td>@Html.DisplayFor(x => x.Projects[i].ProjectNumber)</td>
                    <td>@Html.DisplayFor(x => x.Projects[i].ProjectWorkType)</td>
                    .... // more display properties
                </tr>
             }
         </tbody>
     </table>
     <input type="submit" value="Delete Permanently" />
}

An example of one of the Controller Action methods that is called from this View, and that does not have the same Project that was in the View. Somehow, it has the same number of Projects that were originally selected, but if only one was selected, it has the Project with the lowest Model Id. I'm not sure how else to describe what's happening. But in summary, the correct ViewModel is not making it's way into the POST method example shown below.

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Sys Admin, Account Admin")]
public async Task<ActionResult> DeleteConfirmedMultipleProjects([Bind(Include = "Projects")] ProjectIndexViewModel projectIndexViewModel)
{
    if (ModelState.IsValid)
    {
        // Remove Projects from db and save changes
        return RedirectToAction("../Project/Index");
    }
    return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}

Please help!

amartin
  • 330
  • 2
  • 4
  • 17
  • Wall of code! Could you only post ***Minimum Viable Code***? – Win Jan 04 '17 at 19:34
  • Sorry about that....I cleaned it up a little. – amartin Jan 04 '17 at 19:47
  • what do you mean by "I get the ViewModel, but not with the same properties as the one that populated the View." and what is your expected data in "DeleteConfirmedMultipleProjects" method? – Karthik Ganesan Jan 04 '17 at 19:54
  • The ViewModel that is built and passed to the View from the Action method `EditProjectsTable` works as expected in the View returned from this method. However, when the form in this View is submitted to the method `DeleteConfirmedMultipleProjects`, the same `Projects` are not in the model. But the most confusing part about it is, as I attempted to explain above, the same number of `Projects` that are selected in the initial View are sent to `DeleteConfirmedMultipleProjects`, but for example, if only 1 of the `Projects` was initially selected, and subsequently POST'ed to ... – amartin Jan 04 '17 at 21:05
  • ... `DeleteConfirmedMultipleProjects`, then it is a `Project` with a different Model Id. – amartin Jan 04 '17 at 21:11
  • If the scenario of what I'm trying to do makes sense, and I'm just going about it the wrong way, I'm all for reworking this. It is the first attempt at what will be many instances of related situations in this app. – amartin Jan 04 '17 at 21:41
  • What is the code where you use `//build the ViewModel`? Are you attempting to change the model that was posted e.g deleting `ProjectDetailsViewModel` items from your `Projects` property (which will not work because the values have already been added to `ModelState` unless you use `ModelState.Clear()`)? –  Jan 07 '17 at 10:52
  • @StephenMuecke, yes, I was attempting to modify the `Projects` property to only contain the Projects that were selected with checkboxes on the View that posts the form. I updated the code in that section. – amartin Jan 08 '17 at 17:05

2 Answers2

2

The issue is that when you submit to the EditProjectsTable() method from the first view, the values of all form controls are added to ModelState.

Repopulating your collection of ProjectDetailsViewModel does not update ModelState, and when you return the view, the DisplayFor() methods will display the correct values because DisplayFor() uses the values of the model, however your

@Html.HiddenFor(x => x.Projects[i].ProjectModelId)

will use the values from ModelState, as do all the HtmlHelper methods that generate form controls (except PasswordFor()).

One way to solve this is to call ModelState.Clear() before you return the view in the EditProjectsTable() method. The HiddenFor() method will now use the value of the model because there is no ModelState value.

[HttpGet]
[Authorize(Roles = "Sys Admin, Account Admin, User")]
public async Task<ActionResult> EditProjectsTable(ProjectIndexViewModel projectIndexViewModel)
{
    // Repopulate the Projects collection of ProjectIndexViewModel to
    // include only those that have been selected
    ModelState.Clear(); // add this
    return View(projectIndexViewModel);
}

For a explanation of why this is the default behavior, refer the second part of this answer.

Side note: Your using a view model, so there is no point including a [Bind] attribute in your methods.

Community
  • 1
  • 1
  • @stephenmueke , thanks for your great response. This explains the issue I was having. Do you have any insight into advantages / disadvantages of using this type of scenario over TempData? – amartin Jan 09 '17 at 04:52
  • 1
    TempData can be used, but it only lasts one request, so if you used that solution, then if the user hit F5, everything would be lost (and the user would not have a clue what was going on). Generally, use it only for non critical data (such as passing a friendly success message from a POST method when you redirect) –  Jan 09 '17 at 04:56
  • 1
    Just as a side note, your initial POST method is doing an awful lot of database calls to repopulate your data (I deleted most of that code because it was not relevant to the question), and it may be affecting performance so I would consider caching that data (say by adding it to `Session`) and then getting it again in the `EditProjectsTable` (and then remove it from `Session` before returning the view –  Jan 09 '17 at 04:59
  • Using `ModelState.Clear();` as you described it solved the problem. Thank you, and thanks for the additional reference as well! – amartin Jan 10 '17 at 04:13
0

I think your problems comed from this part:

@Html.CheckBoxFor(x => x.Projects[i].Selected, new { @class = "big-checkbox" })
@Html.HiddenFor(x => x.Projects[i].ProjectModelId)

I had this error before and what I did is adding a Boolean property to ProjectDetailsViewModel like IsSelected. then you should have :

@Html.CheckBoxFor(x => x.Projects[i].IsSelected, new { @class = "big-checkbox" })

Then on method you should add:

foreach (var project in ProjectIndexViewModel.Projects  )
        {
            if (project.IsSelected==true)
                "put your logic here"
        }
Hadee
  • 1,392
  • 1
  • 14
  • 25