ASP.net MVC - Separate ViewModel for POST Action
Asked Answered
C

4

3

In my MVC application I have a View Model that looks similar to this:

public class ComplexViewModel
{
    public ComplexDetailsViewModel Details1 { get; set; }
    public ComplexDetailsViewModel Details2 { get; set; }
}

public class ComplexDetailsViewModel 
{
    public int Id { get; set; }
    public string DisplayValue1 { get; set; }
    public string DisplayValue2 { get; set; }
    // ...
}

I was originally doing the following in my view:

@Html.HiddenFor(model => model.Details1.Id)
@Html.HiddenFor(model => model.Details2.Id)

@Html.DisplayFor(model => model.Details1.DisplayValue1)
...

I would POST the full model to the controller:

 public ActionResult Post(ComplexViewModel model)

I don't actually need anything from ComplexViewModel except for the Id values, so I decided to create another view model used specifically for POSTing the data:

public class PostViewModel
{
    public int Details1Id { get; set; }
    public int Details2Id { get; set; }   
}

public ActionResult Post(PostViewModel model)

The problem is that now my @HiddenFor(model => model.Details1.Id) does not map to my POST model so nothing actually gets POSTed.

Is there a way to have the separate structure for my POST model and my GET model while still using the HiddenFor helper?

Casebook answered 14/12, 2011 at 22:43 Comment(0)
S
1

Just write the HTML for the hidden inputs by hand instead of using the Html Helpers.

<input type="hidden" id="Details1Id" value="@Model.Details1.Id"/>
<input type="hidden" id="Details2Id" value="@Model.Details1.Id"/>

Update

I had issues doing something similar. I ended up flattening out the form related properties on my views. Automapper makes it really easy to map from other objects to your view and can flatten out hierarchies. Doing this, your new view might end up looking similar to this.

public class ComplexViewModel        
{        
    public long Details1Id { get; set; }        
    public string Details1Name { get; set; }
    public long Details2Id { get; set; }    
    public string Details2Name { get; set; }    
}  
Strength answered 14/12, 2011 at 22:55 Comment(3)
I knew I could do this, but I was hoping there would be a way to still use the helpers. It doesn't appear that this will be the case, though.Casebook
You can use @Html.Hidden instead of @Html.HiddenFor() to accomplish this, right?Piloting
Yes but I was hoping to keep using HiddenFor, since it automatically adds a prefix for nested templates.Casebook
C
5

Just because you don't use all of the data in the POST version doesn't mean you have to make another model. Why not keep it simple?

This is how it should work:

Your post details view should be strongly typed to a specific view model. Then in your controller you have two action results named Post for instance, one is decorated with the [HTTPGET] attribute, and the action you want to post to is decorated with the [HTTPPOST] attribute. Additionally, your get method should take a parameter such as post id, and your post method should take the model as parameter.

To enforce server side validation correctly you can decorate your class properties like so:

public class ComplexDetailsViewModel 
{
    [Required]//Works for just the Id property
    public int Id { get; set; }
    public string DisplayValue1 { get; set; }
    public string DisplayValue2 { get; set; }
    // ...
}

Now in your controller you can use this bool: ModelState.IsValid. Basically if they had JavaScript turned off and the model was posted with no Id then the model would be invalid.

This pattern is extremely powerful, and makes client side and server side validation quick to implement. And of course client side validation uses jQuery out of the box so we can easily extend the validators. You can even do AJAX validation very quickly. When I build my forms I don't sacrifice anywhere when it comes to validation.. as it takes no time all to do it correctly.

To answer your original question:

A view can only be strongly typed to one model. You can't load the view with one model, and post it with another (As far as I know). I think if you're trying to do that, your problem is in the way you've built your model.

Carpal answered 14/12, 2011 at 22:51 Comment(6)
I like the simplicity of the POST model because it makes my validators easier to work with. Instead of having complex models that I just have two integers that I can easily work with.Casebook
@Casebook I'll update my answer to give you an example of how you could make your validators work with only the id being required.Carpal
I have the validators working with the original way I wrote it and I don't like them. I rewrote the model to make it a lot cleaner. I'm happy with the way the model is, I just wanted to know if it is possible to use the helpers so they can "map" to a different model property.Casebook
I'm not using DataAnnotations. I'm using FluentValidation and I didn't like the way the validations looked when having the complex properties.Casebook
asp.net mvc doesn't care what class the models are when binding, it only cares about model property names and the names from the input elements on the form. Make them the same on both models and there are no problems. It is easy to do this if you flatten out complex objects for your views. I do this often for partial page updates with ajax. No need to post the full form data if you only need part of it.Strength
Down-vote: You can load with one model and post with another.Microampere
S
1

Just write the HTML for the hidden inputs by hand instead of using the Html Helpers.

<input type="hidden" id="Details1Id" value="@Model.Details1.Id"/>
<input type="hidden" id="Details2Id" value="@Model.Details1.Id"/>

Update

I had issues doing something similar. I ended up flattening out the form related properties on my views. Automapper makes it really easy to map from other objects to your view and can flatten out hierarchies. Doing this, your new view might end up looking similar to this.

public class ComplexViewModel        
{        
    public long Details1Id { get; set; }        
    public string Details1Name { get; set; }
    public long Details2Id { get; set; }    
    public string Details2Name { get; set; }    
}  
Strength answered 14/12, 2011 at 22:55 Comment(3)
I knew I could do this, but I was hoping there would be a way to still use the helpers. It doesn't appear that this will be the case, though.Casebook
You can use @Html.Hidden instead of @Html.HiddenFor() to accomplish this, right?Piloting
Yes but I was hoping to keep using HiddenFor, since it automatically adds a prefix for nested templates.Casebook
M
1

The models need to be more similar. One your "View" model has class properties, and your "Post" model does not. If the names of the class properties, and properties don't match, they don't get bound by the model binder. Try something like:

public class ComplexViewModel 
{ 
  public ComplexDetailsViewModel Details1 { get; set; } 
  public ComplexDetailsViewModel Details2 { get; set; } 
} 

public class ComplexDetailsViewModel : PostDetailsModel
{ 
  public string DisplayValue1 { get; set; } 
  public string DisplayValue2 { get; set; } 
} 

public class PostModel
{
  public PostDetailsModel Details1 { get; set; } 
  public PostDetailsModel Details2 { get; set; } 
}

public class PostDetailsModel  
{ 
  public int Id { get; set; } 
} 

The big problem not using the same models between View and Post is validation. What happens if you want to change validation (assuming using the built in MVC validation for client side and server side). Now you have to change both models! Oh wait... no you don't...

public interface MyValidation
{
  [required]
  public int id { get; set; }
}

Then simply add these validation to your classes: (MetadataType tells MVC what to use for validation)

[MetadataType(typeof(MyValidation))]  
public class ComplexDetailsViewModel ....

[MetadataType(typeof(MyValidation))]
public class PostDetailsModel ....

This certainly would work, but I would HIGHLY discourage anyone from using two different models between views and postbacks. In this instance, I see absolutely no reason why they should be different.

Marenmarena answered 14/12, 2011 at 23:5 Comment(0)
M
0

Easy: simply have your view's view-model inherit from your controller post-model. Then your HiddenFor methods with work, you just need to make sure the properties you are expecting to post are in the base class (the post model).

Microampere answered 5/10, 2016 at 3:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.