Postback values getting lost! - asp.net-mvc

I have a controller with typical create methods (one for GET, one for POST). the POST takes a strongly-typed parameter:
[HttpPost] public ActionResult Create(Quiz entity)
however, when the callback is made, the properties of my entity are null... if I redefine it like this:
[HttpPost] public ActionResult Create(Quiz entity, FormCollection form)
I can see that the values are there e.g. form["Component"] contains "1". I've not had this problem in the past and I can't figure out why this class would be different.
thoughts anyone?

The easiest way to get the default model binder to instantiate Quiz for you on postback is to use the Html form helpers in you view. So, for example, if your Quiz class looked like this:
public class Quiz
{
public int Id { get; set; }
public string Name { get; set; }
}
The following code in your view would ensure the values are present on postback:
#Html.HiddenFor(mod => mod.Id)
#Html.TextBoxFor(mod => mod.Name)
Keep in mind that values which need to be posted back but not shown in the view (like identifiers) need to be added to the view with Html.HiddenFor.
Here's a more comprehensive list of Html form helper functions.

I FIGURED IT OUT!!
so, in my model (see comments on #ataddeini's thread below) you can see I have a Component... to represent components I used a couple of listboxes, the second (Components) dependent on the contents of the first (Products). In generating the second list I used
#Html.DropDownListFor(x => x.Component, ...)
which (as shown in one of the above links) generates a form field called "Component"... and therein lies the problem. What I needed to have done is bind it to the the Id of the component instead!
#Html.DropDowListFor(x => x.Component.Id, ...)
hurray!

Related

Why does my viewmodel name have to be 'model'?

I've been working with MVC for quite a while. I made a form to submit my data using an entity model and as per requirement had to add tags too so I updated the view and the actionmethod to use a viewmodel instead. Like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(PostwTagsVM post)
{
}
Surprisingly, the model was null.I couldn't find out why but then decided to rename the object as below:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(PostwTagsVM model)
{
}
Surprisingly, I get the data in the model now.
I know I can work like this but if i really needed to name by model object something else other than 'model'. Whats happening here?
It does not have to be named model.
If in the first case, your model is null, its because your PostwTagsVM model contains a property named post.
The parameter can be named whatever you want, except that it cannot be the same name as one of the properties in your model.
The reason is that your form would be sending back a name/value pair that is (say) post=someValue. The DefaultModelBinder looks for a matching name, sets the value of the property named Post to someValue, but then also finds a parameter named post and tries to set that to someValue, which fails (because you cannot do PostwTagsVM post = "someValue";), and the model becomes null.

MVC - Inherited view model members [duplicate]

My Post call does not return the correct Model type. It always use the baseObject instead of the correct derived object that I passed in from the Get
RestaurantViewModel.cs
public class RestaurantViewModel{
public Food BaseFoodObject{get;set;}
}
Food.cs
public class Food{
public string Price{get;set;)
}
Bread.cs -- Inherit from Food
public class Bread:Food{
public int Unit{get;set;}
}
Milk.cs -- Inherit from Food
public class Milk:Food{
public string Brand{get;set}
}
Editor For Template for Bread. Display the unit and allow user to edit
Index.html
#Model RestaurantViewModel
#using(Html.BeginForm("SaveFood", "Food"))
{
#Html.EditorFor(m=>m.BaseFoodObject)
<input type="submit" value="Process"/>
}
Bread.cshtml
#Model Bread
<div>
#Html.TextboxFor(bread=>bread.Unit)
</div>
FoodController.cs
public ActionResult Index(){
Bread bread = new Bread(){
Price = "$10",
Unit = 1
}
RestaurantViewModel viewModel = new RestaurantViewModel(){
BaseFoodObject = bread
}
return View(viewModel);
}
public ActionResult Post(RestaurantViewModel viewModelPost)
{
// When I inspect the viewModelPost, there is no attribute for unit
}
Final Result:
1. The display looks correct. EditorFor is smart enough to pick the correct editor template and display the value correctly
2. The Save does not work. The Unit attribute of Bread Object does not get passed in with the RestaurantViewModel. The reason for that is the RestaurantViewModel used the Food object instead of Bread
I hope there is away to modify the EditorFor and tell it to use the Model in the View or the Object Type that I passed in when I display it.
Thanks
Update 1: I solved this problem by using the custom binder and using a factory to decide which object I really want. This helps construct the correct Model which I want
MVC is stateless. A couple of references.
There's a couple of statements in your question that conflict with this, and how MVC binding works eg:
My Post call does not return the correct Model type.
Possibly just terminology, but your Post call does not 'return a model type' - it goes into the model that's defined in the post action, in this case RestaurantViewModel.
instead of the correct derived object that I passed in from the Get
because it is stateless, it knows nothing about the model you passed in from the get... absolutely nothing.
The final html rendered via the getaction+view.cshtml+model is not linked to the postaction. You could just as easily take the rendered html, save it, reboot your PC, reload the rendered html and it will work exactly the same way.
a way to modify the EditorFor and tell it to use the Model in the View or the Object Type that I passed in when I display it
When you use EditorFor it sets an ID and name attribute based on the model it was bound to, so it already does this, but perhaps you are not binding to the model you want to bind to to get the correct id.
So, to the question, if, in 'normal' C# code you were to instantiate a new instance of RestaurantViewModel, what would you expect the type of BaseFoodObject to be?
This is what the ModelBinder is doing - it's creating a new RestaurantViewModel.
As your post action method's signature does not include anything to do with Bread - all the bread properties are ignored.
Some options:
Check for the food properties after binding and read them manually (probably the quickest+easiest but not very "mvc-ish")
public ActionResult Post(RestaurantViewModel viewModelPost)
{
if (!string.IsNullOrEmpty(Request.Form["Unit"]))
// it's a bread form
to make this easier, you could provide a hidden field with the type
if (Request.Form["Type"] == typeof(Bread).Name)
{
var bread = new Bread { Unit = Request.Form["Unit"] }
Add bread to the action so it's bound
public ActionResult Post(RestaurantViewModel viewModelPost, Bread bread)
but then, obviously, it won't work for milk.
So could extend this using an ActionNameSelector to select the correct action
public ActionResult PostBread(RestaurantViewModel viewModelPost, Bread bread)
public ActionResult PostMilk(RestaurantViewModel viewModelPost, Milk milk)
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class FoodSelectorAttribute : ActionNameSelectorAttribute
{
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo)
{
... check if provided parameters contains bread/milk
(related link but not a solution to this specific case)
Another option might be to change the Restaurant type to a generic, but would require a few more changes (and ideally use of interfaces), and more details (provided here as an idea, rather than a solution)
The basics would be:
public class RestaurantViewModel<T>
where T: Food
{
}
public ActionResult Post(RestaurantViewModel<Bread> viewModelPost)
public ActionResult Post(RestaurantViewModel<Milk> viewModelPost)
but I've not confirmed if the default ModelBinder would work in this case.
The problem comes with the post. Once you post, all you have is a set of posted data and a parameter of type, RestaurantViewModel. The modelbinder sets all the appropriate fields on Food because that's all it knows. Everything else is discarded. There's nothing that can be done about this. If you need to post fields related to Bread then the type of your property must be Bread. That's the only way it will work.

Ambiguous action method

For a project I'm currently working on, I currently have 2 separate instances of users (might increase later): CorporateCustomer and PrivateCustomer.
Both inherit from the abstract class Customer.
To display the differences between these customers, currently 2 different views are created, which are rendered by the same Action in the following way:
[HttpGet]
public virtual ActionResult Edit()
{
if(User.IsCorporate)
return View("EditCorporate", new CorporateCustomer());
else
return View("EditPrivate", new PrivateCustomer());
}
[HttpPost]
public virtual ActionResult Edit(CorporateCustomer customer){...}
[HttpPost]
public virtual ActionResult Edit(PrivateCustomer customer){...}
For just displaying information, this works like a charm. The urls are the same for each type, which is what we were aiming for.
However, when doing a post, I can only specify a single type, or risk running into an ambiguous action method (which makes sense, of course).
Now my question is: is there any elegant way to handle these 2 different types, while still retaining a single url? Or am I doomed to make the base class non-abstract and look up the values in the Request.Form collection?
Thanks if anyone can come up with a sollution (or just straight point out that what I'm doing is stupid and cannot be done)
You could have one Action that takes both parameter types.
The model binder should then fill them with whatever data is posted and you can figure out which is right in your Action method.
[HttpPost]
public virtual ActionResult Edit( CorporateCustomer c, PrivateCustomer p )
{
...
}

MVC 3/4 - Form data posted from paartial views and editor templates not reaching the Controller's action

I am working on an MVC 4 site that makes extensive use of partial views. On one page, however, I am using the the same partial view within nested partial views and my Model is nested as well. I checked it out in Fiddler, and the data is being posted as part of the form. When it hits my break point that I've set up in the action method of the controller those nested view models are coming in as null. I've tried using editor templates instead of partial views, but I had no luck on that one.
Has anyone experienced this behavior before, and is so, do you have any ideas as to what might be causing it?
I have had this same problem before because I accidentally passed a nested ViewModel property into my partial page.
If you are nesting partials you need to be careful about how you are passing in your model, for example:
Lets say this is your ViewModel:
public class Person
{
public string Name {get; set;}
public Address Address {get; set;}
}
public class Address
{
public string Line1 {get; set;}
//etc
}
And your controller action:
public ActionResult UpdatePerson(Person p)
{
}
If you have a separate view to display an Address make sure you do it like this:
#Html.RenderPartial("Address", Model)
And not like this:
#Html.RenderPartial("Address", Model.Address)
If you do the second example the name of the "TextboxFor" inputs will be named incorrectly for the model binder to be able to understand.
Another option would be to call out address specifically in your controller action like this to allow the model binder to see the address properly:
public ActionResult UpdatePerson(Person p, Address addr)
{
}

Can you remove the HTML Field Prefix from strongly typed models in MVC 3?

I have a view model like this:
public class EditVM
{
public Media.Domain.Entities.Movie Movie { get; set; }
public IEnumerable<Genre> Genres { get; set; }
}
Movie is the real entity I wish to edit. Genres is simply present to populate a drop down. I would prefer that when I call:
#Html.TextBoxFor(m => m.Movie.Title)
inside my strongly typed view that the input control have a name = "Title" instead of "Movie.Title"
I do not wish to split my view into partial views or lose my strongly typed view by using ViewData or the like.
Is there a way to express to the View that I do not wish to have the Movie. prefix? I noticed that you can set:
ViewData.TemplateInfo.HtmlFieldPrefix = "x";
in the controller, but unfortunately it seems only to allow adding an additional prefix. Setting it to "" does nothing.
Is there any work around for this? Or am I stuck with the unfortunate prefix that isn't really necessary in this case if I wish to keep strongly typed views and lambdas?
Thanks for any help.
Update:
Here's the controller actions to maybe make things a bit clearer.
public ActionResult Edit(int? id)
{
var vm = new EditVM
{
Movie = id.HasValue ? _movieSvc.Find(id.Value) : new Movie(),
Genres = AppData.ListGenres()
};
return View(vm);
}
[HttpPost]
public void Edit([Bind(Prefix = "Movie")]Movie m)
{
_movieSvc.AddOrUpdateMovie(m); //Exceptions handled elsewhere
}
No, in order to do what you want you would have to rewrite the Html helpers, and then you would have to write your own model binder. Seems like a lot of work for minimal gain.
The only choice is a Partial view in which you pass the Movie object as the model. However, this would require you to write your own model binder to have it be recognized.
The reason you have to do m.Movie.Title is so that the ID has the correct name, so the model binder can recognize it as a member of your model.
Based on your update:
Your options are:
Use non-strongly typed helpers.
Use a partial view.
Rewrite the stronly typed helpers
Don't use the helpers at all, and write the values to the HTML
Personally, i'd just use 1 or 2, probably 2.
EDIT:
Based on your update above. Change your code to this (note, Genres does not get posted back to the server, so m.Genres will just be null on postback):
[HttpPost]
public void Edit(EditVM m)
{
_movieSvc.AddOrUpdateMovie(m.Movie); //Exceptions handled elsewhere
}
EDIT:
I did just think of an alternative to this. You could simply do this:
#{ var Movie = Model.Movie; }
#Html.TextBoxFor(m => Movie.Title)
However, if there was a validation error, you would have to recreate your EditVM.
I have a view model like this
I think that you might have some misunderstanding about what a view model is. A view model shouldn't contain any reference to your domain models which is what those Movie and Genre classes seem to be. I mean creating a new class that you suffix with VM and in which you stuff all your domain models as properties is not really a view model. A view model is a class that is specifically designed to meet the requirements of your view.
A much more correct view model would looks like this:
public class EditVM
{
public string MovieTitle { get; set; }
public IEnumerable<GenreViewModel> Genres { get; set; }
}
and in your view you would have:
#Html.EditorFor(x => x.MovieTitle)
#Html.EditorFor(x => x.Genres)
Another option is to either use the TextBox(string name, object value) overload instead of the TextBoxFor:
#Html.TextBox("Title", Model.Movie.Title)
You could also specify the input tag HTML instead of using a helper.
Another option is to take EditVM as your postback parameter. This is what I would do. My post action parameter is always the same type of the .cshtml model. Yes there will be properties like lists that are null, but you just ignore those. It also allows you to gracefully handle post errors as well because if there is an error you'll need to return an instance of that view model anyhow, and have the values they submitted included. I usually have private methods or DB layer that handles retrieving the various lists that go into the ViewModel, since those will be empty on postback and will need to be repopulated, while not touching the properties that were in the post.
With your post method as it is now, if you need to return the same view, you've gotta create a new EditVM and then copy any posted values into it, and still populate the lists. With my method, you eliminate one of those mapping steps. If you are posting more than one thing, are you going to have umpteen different parameters on your post action? Just let them all come naturally into a single parameter typed to the EditVM of the View. While maybe having those null properties in the VM during the postback feels icky, you get a nice predictable consistency between View and postback IMO. You don't have to spend alot of time thinking about what combination of parameters on your post method will get you all the pieces of data from the form.

Resources