1

I have read somewhat on the post-redirect-get design pattern and I'm not sure if it works for my purpose as what I have is an MVC site which is design to look like an application, I have multiple dropdowns on the page which all bind to an integer array as below in my controller:

    [HttpPost]
    public ViewResult ResponseForm(PartyInvites.Models.GuestResponse response, int[] SelectedCustomer)
    {

       return View(response); // works but resets all my selected dropdowns
       // return View(); // gives an error that it can't rebind items in view

    }

My View:

@foreach (Schedule sched in Model.Schedules)
        {
@Html.DropDownList("MySelectedCustomer", new SelectList(sched.Customers, "Id", "FirstName"), "Select A Customer", new { @class = "SelectedCustomer" })

}

The GuestResponse:

public class GuestResponse
    {
        [Required(ErrorMessage = "You must enter your name")]
        public string Name { get; set; }
        public string SomeString = "someString";
        public string Email { get; set; }
        public string Phone { get; set; }
        public bool? WillAttend { get; set; }
        public int SelectedSchedule = 0;
        public int SelectedCustomer = 0;

        public List<Schedule> Schedules
        {
            get
            {
                return new List<Schedule>() { new Schedule() { ScheduleName = "party1", ScheduleId = 1 }, new Schedule() { ScheduleId = 2, ScheduleName = "party2" } };
            }
            set
            {
                Schedules = value;
            }
        }
    }

The SelectCustomer property is a property on the GuestResponse class. All the dropdowns are bound and if I change a few they bind nicely to the int[] SelectedCustomer collection. However I want to return my View back (so it does nothing essentially) but this resets all the dropdowns to their original state as the response was never fully bound because there was multiple dropdowns and MVC couldn't model bind to it. What it the best way of doing this so it maintains state so to speak?

2
  • Show your GuestResponse definition and your view. Commented Mar 31, 2013 at 19:00
  • @DarinDimitrov done above Commented Mar 31, 2013 at 19:03

2 Answers 2

2

The correct way to handle this is to use a view model instead of passing your domain models to the view.

But if you don't want to follow good practices you could generate your dropdowns like this as a workaround:

for (int i = 0; i < Model.Schedules.Count; i++)
{
    @Html.DropDownList(
        "MySelectedCustomer[" + i + "]", 
        new SelectList(
            Model.Schedules[i].Customers, 
            "Id", 
            "FirstName", 
            Request["MySelectedCustomer[" + i + "]"]
        ), 
        "Select A Customer", 
        new { @class = "SelectedCustomer" }
    )
}

The correct way is to have a property of type int[] SelectedCustomers on your view model and use the strongly typed version of the DropDownListFor helper:

for (int i = 0; i < Model.Schedules.Count; i++)
{
    @Html.DropDownListFor(
        x => x.SelectedCustomers, 
        Model.Schedules[i].AvailableCustomers, 
        "Select A Customer", 
        new { @class = "SelectedCustomer" }
    )
}

and your POST controller action will obviously take the view model you defined as parameter:

[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
    // The model.SelectedCustomers collection will contain the ids of the selected
    // customers in the dropdowns

    return View(model);
}

And since you mentioned the Redirect-After-Post design pattern, this is indeed the correct pattern to be used. In case of success you should redirect to a GET action:

[HttpPost]
public ViewResult ResponseForm(GuestResponseViewModel model)
{
    if (!ModelState.IsValid)
    {
        // the model is invalid => redisplay the view so that the user can fix
        // the errors
        return View(model);
    }

    // at this stage the model is valid => you could update your database with the selected
    // values and redirect to some other controller action which in turn will fetch the values
    // from the database and correctly rebind the model
    GuestResponse domainModel = Mapper.Map<GuestResponseViewModel, GuestResponse>(model);
    repository.Update(domainModel);

    return RedirectToAction("Index");
}
Sign up to request clarification or add additional context in comments.

Comments

0

Note: I'm first addressing why it's not binding anything, but that's not addressing the array issue, which I will get to afterwards. Where most people go wrong with MVC is that they do not take advantage of the built-in features of MVC to deal with these situations. They insist on doing foreach's and manually rendering things, but do not take into account the collection status.

The reason why the values are reset is because you are using Html.DropDownList() rather than Html.DropDownListFor(), and you are renaming the posted property name to a different name than your model property name.

You could simply change it to this:

@Html.DropDownList("SelectedCustomer", // note the removal of "My"
    new SelectList(sched.Customers, "Id", "FirstName"), 
    "Select A Customer", new { @class = "SelectedCustomer" })

However, you would not have had this issue, and saved yourself a huge headache if you had just used the strongly typed version.

@Html.DropDownListFor(x => x.SelectedCustomer, 
    new SelectList(sched.Customers, "Id", "FirstName"), 
    "Select A Customer", new { @class = "SelectedCustomer" })

As for the Array, you should use an EditorTemplate for Schedules, and in that EditorTemplate you simply create your html as if it were a single item. That's the great thing about Editor/DisplayTemplates is that they automatically deal with collections.

Create a folder in your Views/Controller folder called EditorTemplates. In that folder, create an empty file called Schedule.cshtml (assuming Schedules is a List or array of Schedule). In that, you have code to render a single schedule.

EDIT:

Darin brings up a good point. I would make a small change to the model and add a Selected property to both Schedule and GuestResponse, then you can use Linq to return the selected schedule and it would simplify things.

EDIT2:

You some conflicts between the problem you've described and the code you've shown. I suggest you figure out exactly what you're trying to do, since your code does not really reflect a viable model for this.

12 Comments

The SelectedCustomer property is an integer in his model, not an array of integers so this won't work at all even if you use DropDownListFor(x => x.SelectedCustomer
@DarinDimitrov - From the question (which is different from his code) "which all bind to an integer array"
look at the GuestResponse model he has shown and the SelectedCustomer property. It's a simple integer. He needs to use a view model as I suggested in my answer.
@DarinDimitrov - I know, which is different from what he explains in his question. That's why I suggested the edit I made above.
No, he doesn't explain different. He has shown his HttpPost action in which he has an additional integer property called MySelectedCustomer to bind to the dropdowns. His model is still different and doesn't contain such property. The correct way to solve this is to use a view model as I already suggested. All the problems people are facing with ASP.NET MVC are stemming from the fact that they are not using view models. All those tutorials over the internet showing domain models being passed to the views are really teaching bad practices.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.