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?

User101
  • 748
  • 2
  • 10
  • 29

2 Answers2

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");
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
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.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • 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` – Darin Dimitrov Mar 31 '13 at 19:31
  • @DarinDimitrov - From the question (which is different from his code) "**which all bind to an integer array**" – Erik Funkenbusch Mar 31 '13 at 19:36
  • 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. – Darin Dimitrov Mar 31 '13 at 19:37
  • @DarinDimitrov - I know, which is different from what he explains in his question. That's why I suggested the edit I made above. – Erik Funkenbusch Mar 31 '13 at 19:38
  • 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. – Darin Dimitrov Mar 31 '13 at 19:39
  • @DarinDimitrov - I don't see a MySelectedCustomer property anywhere in his code. – Erik Funkenbusch Mar 31 '13 at 19:40
  • Look at the signature of his `ResponseForm` HttpPost action. That's where you will see it as second argument. It is not present in his model. That's the whole problem which is preventing him from using the strongly typed version of the `DropDownList` helper.. – Darin Dimitrov Mar 31 '13 at 19:41
  • @DarinDimitrov - That's not called MySelectedCustomer either. It's called SelectedCustomer. – Erik Funkenbusch Mar 31 '13 at 19:42
  • Right, I didn't notice that. His code is even worse then as it won't even bind to his `MySelectedCustomer` dropdown :-) He said in the question that he was able to get the selected values of the dropdowns in his post action but I am afraid that this isn't the case if he has this typo. – Darin Dimitrov Mar 31 '13 at 19:43
  • @DarinDimitrov - While I agree with the view model, If he's going to have multiple dropdownlists for each Schedule item, he should be using an EditorTemplate. – Erik Funkenbusch Mar 31 '13 at 19:45
  • Agreed, he could use an EditorTemplate, but that won't change anything to the problem he is facing if he doesn't use a view model. Also since his `Schedules` property is a `List` he could use a `for` loop to correctly bind the values. I guess that the first step towards correct ASP.NET MVC is to learn to use view models. Editor templates come next. But they are just a refactoring to an already correctly written code where portions of the view coul dbe externalized in reusable parts. – Darin Dimitrov Mar 31 '13 at 19:46
  • @DarinDimitrov - In a perfect world, sure, newbies should learn exactly how the wire format for collections work first, but in practice they seldom do.. which is why I prefer to show them EditorTemplates, because it causes fewer problems with getting the naming correct (at least until they become more advanced, then ET have more to take into account). Regardless, He needs to fix his model first. – Erik Funkenbusch Mar 31 '13 at 19:53