[RESOLVED]Post Backs Using View Models

This problem has been driving me crazy for past few weeks I wonder if anyone has some advice for me.

I have a view model, that contains 5 drop down lists.  When I create the initial view and send it out,  all 5 select lists have values from the viewmodel.  The user can see and select any value in any of them.  However, when I post back only 1 of the select
lists contain any values!

The problem this presents of course, is this, in the post back ActionMethod I first check ModelState.IsValid if it’s not valid I simply return the posted Viewmodel, but guess what those selectlists that had no values on the post now show nothing along with
the errors telling them to select something.

Ever seen this symptom before?

The issue is likely to be around the names the select elements are getting, as to be considered a list of values they need the right indexes in their names, andor your parameter for the POST action.  Post the relevant sections of the view where the lists
are created and the actions too.

One other bit of information…

I have an extension method returning this type  

public static List<SelectListItem>

The view model stages the content like this:

 public List<SelectListItem> EndHourSelectList { get; set; }

In the Validation implementation on the post back  (Locals) I see this:

+EndHourSelectList Count = 0×00000000 System.Collections.Generic.List<System.Web.Mvc.SelectListItem>

Notice the count is zero!  This implies that MVC some how created a new instance doesn’t it?  But if so why did the other one have values?

 

//This one returns values   
@Html.DropDownListFor(p => p.TT.area_worked_in, Model.AreaSelectList)

//these like this do not
@Html.DropDownListFor(p => p.StartHourSelectList, Model.StartHourSelectList);

p is the ViewModel  and TT is a type within the ViewModel

Here’s a Viewmodel example for one of the four that do not post back values in the selectlist

public List<SelectListItem> StartHourSelectList { get; set; }

Finally I build the list like this:

        public static List<SelectListItem> SetTimeTrackMinutes(this List<SelectListItem> SelectList)
        {
            if (SelectList == null) throw new InvalidOperationException("STTM300-Must Initialize the list before calling this method");
            SelectList.Add(new SelectListItem { Selected = true, Text = "Select Minutes (15 min incr.)", Value = SelectListValues.NotSelected.ToString() });
            SelectList.Add(new SelectListItem { Selected = false, Value = "0", Text = "00 (Minutes)" });
            SelectList.Add(new SelectListItem { Selected = false, Value = "15", Text = "15 (Minutes)" });
            SelectList.Add(new SelectListItem { Selected = false, Value = "30", Text = "30 (Minutes)" });
            SelectList.Add(new SelectListItem { Selected = false, Value = "45", Text = "45 (Minutes)" });
            return SelectList;
        }

I think you’re telling me I need a POCO class to model the minutes in order for the minutes to have Names?

You need a property on the view model that holds the current item

public class ViewModel
{
    public string SelectedHour { get; set; }
    public List<SelectListItem> StartHourSelectList { get; set; }
}

Controller

public ActionResult Index()
{
    ViewModel model = new ViewModel();

    SetupModel(model);

    return View(model);
}

[HttpPost]
public ActionResult Index(ViewModel model)
{
    SetupModel(model);

    return View(model);
}

private void SetupModel(ViewModel model)
{
    model.StartHourSelectList = new List<SelectListItem>();
    model.StartHourSelectList.SetTimeTrackMinutes();
}

View

@using(Html.BeginForm())
{
    @Html.DropDownListFor(p => p.SelectedHour, Model.StartHourSelectList);
    <p>
        <input type="submit" value="submit"/>
    </p>
}

Thanks Aidy;

I’ll give it a try and post back results.

Aidy;

 One question, my current post looks like this:

  @using (@Html.BeginForm("TimeTrackingForm", "ControllerName", FormMethod.Post))

Representing the TimeTrackingFrom action method in the controller that takes a Viewmodel as input parm like this:

public ViewResult TimeTrackingForm(VMTimeTrackForm TTF)

The view is strongly typed to the VMTimeTrackFrom type, so shouldn’t this be all I need on the @Html.BeginForm?

Ok Aidy very good you helped me solve this infuriating problem… I think I have a better understanding of the secretive Razor binding engine now. 

Here’s the general set of rules I’ll be following from now on.

  1. Any view model that uses an IEnumberable<ofSomething> and a DropDownListFor (or other HTML.Helper method) must specify in the first parameter where to place the selected value like this ->  DropDownListFor(p=>StuffTheSelectedValue, TheSelectList), where
    p is the Viewmodel.  The variable "StuffTheSelectedValue" in my case was a string with public Getter, Setters. [I didn't have it at first]
  2. If you don’t get that first parm right, MVC will return NOTHING for the SelectItemList itself even if it wen’t out the first time ok.  It will not signal an error anywhere in the compile, or the post back, rather it will just show up with nothing.  This
    is most likely done by the binder part of Razor whereby prior to seeing the post back in the controller, it initializes a new instance of the object, parses the querystrings and plugs the values by name where they belong.  If it can’t find anything to plug
    it leaves it blank.
  3. If you have implemented IValidatableObject the view state will most likely be invalid if the first parm was wrong, provided you are checking for something relevant or the model has proper data annotations.
  4. If the controller checks the view state and simply returns the view when the state is invalid, it will NO LONGER have any items to select!  The user will see the error messages but will not be able to do anything for them.
  5. You do not have to re-initialize the view content, if and only if, you get the first parameter correct in the DropDownListFor correct. (Or any other HTML "helper" method)
  6. If you just return the invalid view from the POST back and
    everything is correct (meaning the binder found everything to fill in)
    ,  all of the content is still there.

I too, after debugging this for a long time, had created my own post back initializes, but will not do that any more as it is a sign that something else isn’t right.  At a minimum, to me anyway, there should be some type of warning at compile time that this
could happen.  

Symptoms of this problem are :

  1.  PostBacks loose data and require re-initialization of the ViewModel.
  2. Users see data annotation or other ValidationResult errors but have no way to fix them.
  3. You find when breaking into the PostBack (very first line in the Action Method) in the controller that the viewmodel is missing data.
  4. You are writing re-initialization routines just for Post Backs.

Solution:

  1. The query strings that are returned are name/value pairs, if there is no name of MVC on the server side it will not do anything which is correct behavior.
  2. If you are using strongly typed views your BeginForm only needs to specify the controller post back action method that takes that strong type as it’s first parm.
  3. This then is the MVC method for maintaining ViewState without using ViewState.

My final solution was this in the view:

  @Html.DropDownListFor(p => p.SelectedStartHour, Model.StartHourSelectList);

This in the View Model:

public List<SelectListItem> StartHourSelectList { get; set; }
public string SelectedStartHour { get; set; }

Notice the first parm on the DropDownListFor pointed to a string value in the ViewModel, simple and it relieved me of a lot of pain in trying to understand this MVC thing.

One last thing Aidy, 

I got rid of all my re-initialization logic in the Post back…  it’s wasn’t needed any longer.  And Thanks a bunch for your help.

Leave a Reply