Pulldown values not filled in in ViewModel

2019-08-22 11:13发布

问题:

I am working through Hanselman's tutorial on MVC, and attempting to use a view model in the Create and Edit views so as to fill in a pulldown menu. In this we are directed to create a table Dinner, with columns including Title and Country and the related classes, and then a ViewModel containing a dinner object and a SelectList for a pull-down menu thus:-

public class DinnerFormViewModel {
    public Dinner     Dinner    { get; private set; }
    public SelectList Countries { get; private set; }

    public DinnerFormViewModel (Dinner dinner) {
        Dinner    = dinner;
        Countries = new SelectList(...code to create list of countries..., dinner.Country);
    }
}

This I use to create Edit and Create methods in the controller. The Edit method looks like this:-

public ActionResult Edit(int id, FormCollection formValues) {
    Dinner dinner = dinnerRepository.GetDinner(id);

    try  {
        UpdateModel(dinner);
        dinnerRepository.Save();
        return RedirectToAction("Details", new { id = dinner.DinnerId });
    }
    catch { ...etc... }
}

and the code generated in the view looks like this:-

<div class="editor-field">
    <%: Html.EditorFor(model => model.Dinner.Title) %>
    <%: Html.ValidationMessageFor(model => model.Dinner.Title) %>
</div>

<div class="editor-field">
    <%: Html.DropDownList("Country", Model.Countries) %>
    <%: Html.ValidationMessage("Country", "*") %>
</div>

So far, so good. But when it comes to the Create method it starts to get a bit strange. While the code in the view is essentially identical, when I invoke the UpdateModel method to fill in the dinner object, the only field that is filled in is the Country, from the pull-down menu; when I try it with the DinnerFormViewModel, the Country in the Dinner is left null, but all the other fields are filled in. So the Create method looks like this:-

public ActionResult Create(FormCollection formValues) {
    Dinner              dinner              = new Dinner();
    DinnerFormViewModel dinnerFormViewModel = new DinnerFormViewModel(new Dinner());

    try {
        UpdateModel(dinnerFormViewModel);
        UpdateModel(dinner);
        dinnerFormViewModel.Dinner.Country = dinner.Country;
        dinnerRepository.Add(dinnerFormViewModel.Dinner);
        dinnerRepository.Save();
        return RedirectToAction("Details", new { id = dinnerFormViewModel.Dinner.DinnerId });
    }
    catch {...etc...}
}

While this works, it doesn't to me look like it is the way I should be writing this code. Is it right, or if it isn't, what should I be doing? (It is a thousand pities that the example given in the tutorial doesn't compile.)

回答1:

Html.EditorFor(model => model.Dinner.Title)

This maps automatically to your view model because the HTML name generated will know how to map to Dinner.Title

Html.DropDownList("Country", Model.Countries)

This will give the HTML element the name "Country". This means it expects a model.Country when posting to your view model, which doesn't exist on your ViewModel, but does exist in Dinner. Hence UpdateModel(dinnerFormViewModel); does not work but UpdateModel(dinner); does work.

It's all based on the name that is generated for the input elements as far as how it decides assignment into the view model. I often inspect the generated HTML in the browser to sanity check what is being generated when there are problems mapping values on POST.

You could use DropDownListFor so you could specify the model.Dinner.Country so that it would generate a name that is fully qualified for the nested relationship.

So now you understand why it didn't work the way you wanted it to. From here, how you solve it more practically has many variations that I've seen commonly used, but most will involve some sort of mapping of the view model to the entity, and often in another layer such as a business layer, data access layer, or leveraging a tool such as AutoMapper.

You could add a Country property to the root of your ViewModel.

Or I would prefer making your view model more flat where the properties are not nested in Dinner, and after it posts, map your view model to your entity. It looks like Dinner is your entity. I never include my entity in my view model, and keeping entities out of controllers and views is a pretty common practice. Instead there is a Entity->ViewModel assignment at some point during GET, and then a ViewModel->Entity mapping on post. This will make more sense as you eventual move to a layered approach.

By mapping I mean basic assignment. For example, when retrieving your entity, you could map it to your view model like so:

return repo.Get(someId).Select(e => 
   new DinnerFormViewModel {
       HostName = e.HostName,
       // here is an example of flattening out a related entity relationship
       CountryId = e.Location.CountryId, 
       Cost = e.Cost       
   });

Of course I made up some properties because I don't know what your entity looks like.

This allows you to refactor your DB relationships, and you only need to change your mapping.