4

What are good strategies for rebuilding/enriching a nested or complex ViewModel?

A common way to rebuild a flat ViewModel is shown here

But building and rebuilding a nested ViewModel using that method is too complex.

enter image description here

Models

public class PersonInfo
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Nationality { get; set; }
    public List<Address> Addresses { get; set; }
}

public class Address
{
    public int AddressTypeID { get; set; }
    public string Country { get; set; }
    public string PostalCode { get; set; }
}

public class AddressType
{ 
    public int Id { get; set; }
    public string Description { get; set; }
}

view models

public class PersonEditModel
{
    public int Id { get; set; }
    public string Name { get; set; } //read-only
    public int Nationality { get; set; }
    public List<AddressEditModel> Addresses { get; set; }
    public List<SelectListItem> NationalitySelectList { get; set; } //read-only
}

public class AddressEditModel
{
    public int AddressTypeId { get; set; }
    public string AddressDescription { get; set; } //read-only
    public string Country { get; set; }
    public string PostalCode { get; set; }
    public List<SelectListItem> CountrySelectList { get; set; } //read-only
}

actions

public ActionResult Update(int id)
{
   var addressTypes = service.GetAddressTypes();
   var person = service.GetPerson(id);
   var personEditModel= Map<PersonEditModel>.From(person);

   foreach(var addressType in addressTypes)
   {
      var address = person.Addresses.SingleOrDefault(i => i.AddressTypeId == addressType.Id)
      if(address == null)
      {
          personEditModel.Addresses.Add(new AddressEditModel
          {
              AddressTypeId = addressType.Id
          });
      }
      else
      {
          personEditModel.Addresses.Add(Map<AddressEditModel>.From(address));
      }
   }

   EnrichViewModel(personEditModel, person, addressTypes); //populate read-only data such as SelectList
   return Index(personEditModel);
}

[HttpPost]
public ActionResult Update(PersonEditModel editModel)
{
   if(!ModelState.IsValid)
   {
       var person = service.GetPerson(editModel.Id);
       var addressTypes = service.GetAddressTypes();
       EnrichViewModel(editModel, person, addressTypes); 
       return View(editModel);
   }

   service.Save(...);
   return RedirectToAction("Index");
}

//populate read-only data such as SelectList
private void EnrichViewModel(PersonEditModel personEditModel, Person person, IEnumerable<AddressType> addressTypes)
{
    personEditModel.Name = person.Name;
    personEditModel.NationalitySelectList = GetNationalitySelectList();

    foreach(var addressEditModel in personEditModel.Addresses)
    {
        addressEditModel.Description = addressTypes.Where(i => i.Id = addressEditModel.AddressTypeId).Select(i => i.Description).FirstOrDefault();
        addressEditModel.CountrySelectListItems = GetCountrySelectList(addressEditModel.AddressTypeId);
    }
}

My code for building and rebuilding the ViewModels (PersonEditModel and AddressEditModel) is too ugly. How do I restructure my code to clean this mess?

One easy way is to always build a new view model instead of merging/rebuilding since MVC will overwrite the fields with the values in ModelState anyway

[HttpPost]
public ActionResult Update(PersonEditModel editModel)
{
   if(!ModelState.IsValid)
   {
       var newEditModel = BuildPersonEditModel(editModel.Id);
       return View(newEditModel);
   }

but I'm not sure that this is a good idea. Is it? Are there other solutions besides AJAX?

Community
  • 1
  • 1
LostInComputer
  • 15,188
  • 4
  • 41
  • 49
  • Do you really need to re-set the `personEditModel.Name` and `addressEditModel.Description` properties - aren't they posted back with the model, and why not have `CountrySelectList` in `PersonEditModel` so it only needs to be set once, rather that for each address? - Your `EnrichViewModel` would be only 2 lines with one parameter –  Sep 02 '14 at 10:02
  • Name and Address.Description are read only so no. If I place Name and Address.Description in a hidden field, then I just made MVC behave like WebForm where the state of the controls is persistent. Yes, CountrySelectList can be moved to PersonEditModel. – LostInComputer Sep 02 '14 at 10:39
  • And what about cases where I have 10+ readonly fields. I seems inefficient to send all of them back to the server – LostInComputer Sep 02 '14 at 10:41
  • I assumed that you were creating input elements and setting `readonly="readonly"` in which case they will post back. Are you just rendering these in a `span` or `div` or using `disabled="disabled"? –  Sep 02 '14 at 10:48
  • Some are input while some are ,

    or just plain text

    – LostInComputer Sep 02 '14 at 10:53
  • And what about cases where CountrySelectList differs per address type? It does happen from time to time – LostInComputer Sep 02 '14 at 10:55
  • In that case, I think what you doing is fine. –  Sep 02 '14 at 11:02

1 Answers1

3

I'm going to tackle your specific pain points one-by-one and I'll try to present my own experience and likely solutions along the way. I'm afraid there is no best answer here. You just have to pick the lesser of the evils.

Rebuilding Dropdownlists

  • They are a bitch! There is no escaping rebuilding them when you re-render the page. While HTML Forms are good at remembering the selected index (and they will happily restore it for you), you have to rebuild them. If you don't want to rebuild them, switch to Ajax.

Rebuilding Rest of View Model (even nested)

  • HTML forms are good at rebuilding the whole model for you, as long as you stick to inputs and hidden fields and other form elements (selects, textarea, etc).

  • There is no avoiding posting back the data if you don't want to rebuild them, but in this case you need to ask yourself - which one is more efficient - posting back few extra bytes or making another query to fetch the missing pieces?

  • If you don't want to post back the readonly fields, but still want the model binder to work, you can exclude the properties via [Bind(Exclude="Name,SomeOtherProperty")] on the view model class. In this case, you probably need to set them again before sending them back to browser.

    // excluding specific props. note that you can also "Include" instead of "Exclude".
    [Bind(Exclude="Name,NationalitySelectList")]
    public class PersonEditModel
    {
        ...
    
  • If you exclude those properties, you don't have to resort to hidden fields and posting them back - as the model binder will simply ignore them and you still will get the values you need populated back.

  • Personally, I use Edit Models which contain just post-able data instead of Bind magic. Apart from avoiding magic string like you need with Bind, they give me the benefits of strong typing and a clearer intent. I use my own mapper classes to do the mapping but you can use something like Automapper to manage the mapping for you as well.

  • Another idea may be to cache the initial ViewModel in Session till a successful POST is made. That way, you do not have to rebuild it from grounds up. You just merge the initial one with the submitted one in case of validation errors.

I fight these same battles every time I work with Forms and finally, I've started to just suck it up and go fully AJAX for anything that's not a simple name-value collection type form. Besides being headache free, it also leads to better UX.

P.S. The link you posted is essentially doing the same thing that you're doing - just that its using a mapper framework to map properties between domain and view model.

Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • I use Edit Models too but I didn't show it in my question. What I have is PersonFormModel which contains PersonInputModel (my edit model) and AddressFormModel which contains AddressInputModel but rebuilding hierarchical view models is painful! Thanks for your answer. – LostInComputer Sep 06 '14 at 02:45