Mohamed
Mohamed

Reputation: 73

Mixing model binding with form posting in MVC 5?

Is it possible to enable model binding along with a posted data from a form?

I have a collection property that I want to iterate in a foreach loop to save each selected item in the collection:

    <div class="form-group">
        @Html.LabelFor(m => m.Users, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @{
                List<ApplicationUser> allUsers = ViewBag.AllUsers;
                bool assigned;
            }
            <div>
                @foreach (var user in allUsers)
                {
//here I want to render all users, and only the users who are in the task, have a checked checkbox

                    assigned = Model.Users.Select(u => u.Id).Contains(user.Id);
                    <input type="checkbox" name="asndUsers" value="@user.Id" id="@user.Id" @Html.Raw(assigned ? "checked" : "") /> <label style="font-weight: normal;" for="@user.Id">@user.UserName</label><br />
                }
            </div>
        </div>
    </div>
//fields updated with model binding:
    <div class="form-group">
        @Html.LabelFor(m => m.Status, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.EditorFor(m => m.Status, new { htmlAttributes = new { @class = "form-control" } })
            @Html.ValidationMessageFor(m => m.Status)
        </div>
    </div>

this is the Edit action post method:

[HttpPost, ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,Title,Description,DueDate,Status")] UserTask task, string[] asndUsers)
{
    if (ModelState.IsValid)
    {
        task.Users = new List<ApplicationUser>();
        foreach (var item in asndUsers)
        {
            var user = context.Users.Find(item);
            task.Users.Add(user);
        }
        context.Entry(task).State = EntityState.Modified;
        context.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(task);
}

it works when I debug, I see the new posted data has merged with the bound data. but when the request redirected to the Index view, there is no change after editing an item, this is the Index action method:

public ActionResult Index(int? taskId)
{
    var viewModel = new TasksUsers();
    viewModel.Tasks = context.Tasks.Include(x => x.Users);
    if (taskId != null)
    {
        viewModel.Users = viewModel.Tasks.Where(t => t.Id == taskId).Single().Users;
        ViewBag.Row = taskId;
    }
    return View(viewModel);
}

Upvotes: 0

Views: 295

Answers (2)

Mohamed
Mohamed

Reputation: 73

Original answer

The proper way of updating the related entities is loading them first, so I used eager loading to redefine the incoming task parameter as follows:

task = context.Tasks.Include(t => t.Users).Single(s => s.Id == task.Id); Note that `Find` can't be used with `Include` so I used

Single.

That resolved the problem of updating the Users entity

this was wrong, the proper way is to use explicit binding instead of implicit binding (TryUpdateModel())

Upvotes: 1

Georg Patscheider
Georg Patscheider

Reputation: 9463

The task that is posted back is no longer tracked by the DbContext. Try to Attach the task to the DbSet in the Edit action:

    context.Tasks.Attach(task);
    if (task.Users == null) {
        task.Users = new List<ApplicationUser>();
    }
    foreach (var item in asndUsers) {
        var user = context.Users.Find(item);
        task.Users.Add(user);
    }
    // may be important because Attach() sets the State to 'Unchanged'?
    context.Entry(task).State = EntityState.Modified; 
    context.SaveChanges();

As a side note, you can pass parameters when you call RedirectToAction. (Only do this if you want to pass the id of the edited task to the Index action):

return RedirectToAction("Index", new { taskId = existingTask.Id });
//                                     ^^^^^^
//                                     must match parameter name of Index action

Upvotes: 0

Related Questions