MVC Send back model properties after POST - c#

I have a Model that I send to the view in a GET method and is bounded successfully to TextFor and ListFor's
e.g.
#Html.ListBoxFor(x => x.MultiChoice, new MultiSelectList(Model.property, "Id", "Name"), new { multiple = "multiple", #class = "multiselect" })
When the user submits the form, the Model is successfully passed back to the POST action method with its properties.
[HttpPost]
public ActionResult POST(Model quiz)
{
string Q1 = quiz.Q1 // = will equal what the user has put in. good
return View("Quiz", quiz);
}
However, when the Model (quiz) is returned to the view, the properties inside the quiz model are NULL, how do I retain the properties that come through to the POST method?
** Edit **
The GET Method
[HttpGet]
public ActionResult Quiz()
{
try
{
Quiz quiz = new Quiz();
// Of course, I could do this in the constructor of the model..
InitialiseQuiz(Quiz);
return View("Quiz", quiz");
}
catch (Exception ex)
{
}
}
Thanks

If I understood well, when you do this:
#Html.ListBoxFor(x => x.MultiChoice, new MultiSelectList(Model.property, "Id", "Name"))
the Razor will create a <select> tag with x.MultiChoice values as selected options. BUT, nowhere will be persisted the Model.property values (as it may be a collection, right?).
So, when you do the POST, you will only send the x.MultiChoice value back to the server, and the Model.property collection will be missed.
Knowing that, you just need to fill this property with the collection again during the POST action, like:
[HttpPost]
public ActionResult POST(Model quiz)
{
// some code here
quiz.property = new CollectionOfSomething();
return View("Quiz", quiz);
}
Is that what you are looking for?

The modelbinder news up an instance of that class with whatever POST data it has. Anything that's null has no posted data. The easiest way to make it not be null, then, is to create an HTML input for that property so that something is posted for it.
However, in situations where you're dealing with existing data, it's preferable to only post what you need to post, and then lookup the original object again in order to map the original property values back onto the version that was passed into your action:
[HttpPost]
public ActionResult Quiz(int id, Quiz model)
{
var quiz = db.Quizzes.Find(id);
// assuming `Foo` was a property that was not posted
model.Foo = quiz.Foo
...
}

Related

Persist model field from GET to POST

In the controller's GET method, I pick up a parameter (id) from the query string and assign it to a property in the model, which then gets sent to the view.
The view contains a form in which the user will supply the rest of the values for this model. When the model then gets sent back to the POST method, I need that original parameter (id) back again. But I'm not sure how to persist it. In the POST method, the id field is coming back blank. Is there a better way to do this?
// GET: CreateInsured
[Route("Home/CreateInsured/{id}")]
public ActionResult CreateInsured(int id)
{
Insured insured = new Insured();
insured.PolicyId = id;
return View(insured);
}
// POST: CreateInsured
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateInsured(Insured insured)
{
//insured.PolicyId is empty
}
You should put this Id into a Hidden field, like this:
#Html.HiddenFor(m => m.PolicyId)
And this field should be into your form.

Can I use ModelBinder to get object without adding parameter to action?

I am new in MVC. I have a view that contains many dropdowns and viewmodel with many lists. Most of these lists are static so I want to cache retrived records. I can do this with (custom) Model binders, but I dont like that to actually use (custom) Model Binder I need to add parameter for every object in Action method like this:
public ViewResult SubmitNewValue(MyViewModel viewMod, IEnumerable<List1> list1, IEnumerable<List2> list2, IEnumerable<List3> list3 ...)
{
viewMod._list1 = list1;
viewmod._list2 = list2;
viewMod._list3 = list3;
...
return View(viewMod);
}
What I think would look better is something like:
public ViewResult SubmitNewValue(MyViewModel viewMod)
{
viewMod._list1 = ModelBinders.GetInstanceFor<List1>();
viewmod._list2 = ModelBinders.GetInstanceFor<List2>();
viewMod._list3 = ModelBinders.GetInstanceFor<List3>();
//I am able to wrap above to separate function like PrepareViewModel(viewMod)
...
return View(viewMod);
}
Is there a function/way to do this?
I am also not sure if I chose good approach but my viewmodel is losing lists for dropdowns, so I need to somehow readd them to ViewModel after [HttpPost] for example.
The modelbinder's sole job is to bind data from the request (querystring or post body) to parameters on your action method. So, no, if there's no parameter, the modelbinder does nothing with the data.
You can access the data directly from the request, i.e. Request["list1"]. However, that's the raw data as the modelbinder would receive it, before it actually did any of the work it does. In other words, you would need to manually do type-coercion or new up things, etc., with that data.
That said, this sounds like an XY problem. What is it that you're actually trying to achieve, because there's probably a better way.
You can use the ControllerBase.ViewBag Property to store resources for the view to use.
public class MyController : Controller {
public ActionResult SubmitNewValue() {
populateDropDownLists();
MyViewModel model = GetDefaultViewModel();
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult SubmitNewValue(MyViewModel viewMod) {
processMyViewModel(viewMod);
populateDropDownLists();
return View(viewMod);
}
void populateDropDownLists() {
//These could be cached lists
ViewBag.List1 = GetList1();
ViewBag.List2 = GetList1();
ViewBag.List3 = GetList1();
}
}
And then in your view you can access the lists to populate your dropdowns
#Html.DropDownListFor(m => m.MyViewModelProperty, (IEnumerable<SelectListItem>)ViewBag.List1, "Select...", new { #class = "form-control select" })
The view model should not be responsible for storing the lists. Since the lists are only for populating the dropdowns in the view then they should be seperated out

MVC 4 EF - Product / Multiple Categories

EDIT: I think I need some sort of view model, but I'm unsure how to handle this relationship.
I'm trying to understand MVC 4 and EF Code First and I'm trying to map many to many relationships.
I have two classes;
public class Asset
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public virtual ICollection<Category> Categories { get; set; }
}
public class Category
{
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public string Description { get; set; }
public virtual ICollection<Asset> Assets { get; set; }
}
So I'm trying to allow each Asset to have multiple Categories and each Category may have multiple Assets.
On my create method I have;
public ActionResult Create()
{
var model = new Asset();
model.Categories = _db.Categories.ToList();
return View(model);
}
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
#model MyProject.Models.Asset
#using (Html.BeginForm("Create", "Assets", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div>
#foreach (var item in Model.Categories)
{
<p>#item.CategoryName</p>
}
</div>
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
</div>
</div>
}
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
My create return (here my assets categories is null)
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View();
}
try
{
//do stuff
}
catch
{
return View();
}
}
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
In my view, the only way I can show these categories is to say; (Note the capital M in Model. I can't use the lower case model as used elsewhere in the view)
I've always hated that Microsoft uses the model => model.* convention through its generated views and tutorials and articles online; it only leads to confusion. In your view Model is an actual object instance, namely an instance of what you defined as the "model" for the view. The lowercase model you see used in things like Html.EditorFor is actually a parameter to a lambda expression. It can be called anything. For example, Html.EditorFor(x => x.Foo) and even Html.EditorFor(supercalifragilisticexpialidocious => supercalifragilisticexpialidocious.Foo) would work just as well. Although the value that gets passed into this parameter is usually the Model object instance, Model and model are totally different concepts.
When the initial create is called, I can see my asset and it has categories. On the return create method however, its null. I can't work out why. I understand I'm not doing anything to edit these categories in the View, I can't get that far. What I don't understand though is why my model leaves with categories, but comes back with none.
That is why. You're not doing anything to edit these categories in the view. There's no fields for them to be posted along with the form data, and as a result, the class instantiated by the modelbinder in your action does not contain any data for categories. This is key. The class instance that goes into the view is not the same class instance that comes back after something like a post. Each is a unique thing. The post action has no knowledge of anything that came before it; it simply has whatever data was posted. Assuming the action takes a parameter of a particular class, the modelbinder will attempt to new up an instance of that class and bind the posted data to appropriate properties on that class. It doesn't care what was sent to the view originally; it doesn't even care what class it's working with.
Ultimately when creating an Asset, I want to be able to list all the categories and allow some selection as to which categories this new asset will belong. If someone could help me work that out, you're my hero. But if I could just understand why what's coming back isn't what I sent out, that would be a start.
This is the fun part. First, you absolutely must use a view model for this. In case you're not familiar with view models, they're simply classes that are used as a model for a view, hence the name. What you're passing around here, Asset, is technically an "entity", which is a class that is used for data transfer, usually to/from a database. While an entity could be used as a model for a view, as you've done here, it's not really suited for that.
There's a clear conflict of interest, as the needs of a class representing some table schema in a database are vastly different from the needs of a class representing data for a UI layer. That's where view models come in. In the most traditional sense, a view model simply represents the data that will need to be displayed and/or edited in one or more views. It may have many properties in common with a particular entity or it may only have a subset of those properties or even completely different properties. It is the job of your application to "map" from your entity to your view model and vice-versa, so that the logic for saving an entity to a persistence store can be completely abstracted from the logic for how the user interacts with that data.
The reason a view model is so important for your purposes here is that form elements in HTML have certain limitations. They can only work with data that can be represented as a string: things like ints, bools, actual strings, etc. They are particularly unsuited for working with complex objects, like your Category class. In other words, it would be perfectly achievable to post back a list of integer ids, representing Categorys, but it is entirely implausible to post back complete Category instances that have been chosen by a user.
Since your entity expects a list of categories and your view will only feasibly be capable of posting a list of ints, there's a fundamental disconnect. Using a view model provides a way to bridge the gap. Plus, it allows you to have other properties, like a list of category choices to populate your select list with, that would be totally inappropriate to put on your entity class.
For your scenario, you'd need a view model like:
public class AssetViewModel
{
// any other asset properties you need to edit
public List<int> SelectedCategoryIds { get; set; }
public IEnumerable<SelectListItem> CategoryChoices { get; set; }
}
This then allows you to create a multiselect list in your view using:
#Html.ListBoxFor(m => m.SelectedCategoryIds, Model.CategoryChoices)
Now, to populate your view model with data from your entity. In a create view, the entity doesn't exist yet, so you don't need to do any mapping. The only thing you need to do is populate your CategoryChoices property so the select list in the view has some data. However, based on the above discussion about data needing to be posted back or else it will be null, since the actual contents of the select list will never be posted, you'll need to populate this in each of your create and edit actions, both for GET and POST. As a result, it's best to factor this logic out into a private method on your controller that each action can call:
private void PopulateCategoryChoices(AssetViewModel model)
{
model.CategoryChoices = db.Categories.Select(m => new SelectListItem
{
Value = m.Id,
Text = m.Name
};
}
Then, in your create GET action, you'll just new up your view model and populate your category choices:
public ActionResult Create()
{
var model = new AssetViewModel();
PopulateCategoryChoices(model);
return View(model);
}
In the post version, you'll now need to map the posted data onto your Asset entity:
[HttpPost]
public ActionResult Create(AssetViewModel model)
{
if (ModelState.IsValid)
{
var asset = new Asset
{
Title = model.Title,
Description = model.Description,
// etc.
Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id))
}
db.Assets.Add(asset);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateCategoryChoices(model);
return View(model);
}
The edit GET action is similar to the create version, only this time, you have an existing entity that will need to be mapped onto an instance of your view model:
var asset = db.Assets.Find(id);
if (asset == null)
{
return new HttpNotFoundResult();
}
var model = new AssetViewModel
{
Title = asset.Title,
Description = asset.Description,
// etc.
SelectedCategoryIds = asset.Categories.Select(m => m.Id).ToList()
};
Likewise, the edit POST action is similar to the create version, but you're going to map from your view model on to an existing asset instead of creating a new asset. Additionally, because you have a many to many relationship, you have to take extra care when saving the categories.
// map data
asset.Title = model.Title;
asset.Description = model.Description;
//etc.
// You might be tempted to do the following:
// asset.Categories = db.Categories.Where(m => model.SelectedCategoryIds.Contains(m.Id));
// Instead you must first, remove any categories that the user deselected:
asset.Categories.Where(m => !model.SelectedCategoryIds.Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Remove(m));
// Then you need to add any newly selected categories
var existingCategories = asset.Categories.Select(m => m.Id).ToList();
db.Categories.Where(m => model.SelectedCategoryIds.Except(existingCategories).Contains(m.Id))
.ToList().ForEach(m => asset.Categories.Add(m));
The extra footwork here is necessary to prevent saving the same relationship twice, resulting in an integrity error. By default Entity Framework creates a join table for many to many relationships that consists of a composite primary key composed of the foreign keys to each side of the relationship.
The reason why your categories is null is because you are not binding it on the POST. They are not in fields during the POST.
Try this and see if they are filled out:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.TextBoxFor(model => model.Categories[i].CategoryId)
#Html.TextBoxFor(model => model.Categories[i].CategoryName)
}
1.
return View();
you are not passing back the model in your Create Method that is why you don't see the Model is NULL.
// POST: Assets/Create
[HttpPost]
public ActionResult Create(Asset model)
{
if (!ModelState.IsValid)
{
//error, return to view.
return View(model);
// If you don't pass back the model to you view you will see model is NULL
}
try
{
//do stuff
}
catch
{
return View(model);
}
}
Cateogries Will always be null in your case, as you can't post back a List like you are doing in your case.
Try Displaying them in a loop, Then the MVC model Binder will be able to bind your list to Model:
#for (int i = 0; i < Model.Categories; i++)
{
#Html.HiddenFor(model => model.Categories[i].CategoryId)
}
If you want to save SelectedCategories you will have to use MultiSelect

Casting (int)enum to a object property does not get passed when called by razor view model

Using MVC 5 and Razor pages.
The scenario is when I cast (int)colour to a Item property called Colour which is of type INT in my HttpGet method. This value should be passed to the ViewModel, and i store it as a hiddenfield on razor view.
So when debugging my code:
return View(viewModel); the viewModel.Colour = 23 - which is correct.
This will now display the Razor view, and inside this razor view i store the model.House value into a hidden field, so i can perform a HttpPost.
At this stage my raw URL is:
applicationName/Orange/Item/Create?date=2014-08-05&_1236723545
Notice there is Orange in the Colour param in the url, and also a model which is at the end, which has a Colour property, which has the number 23.
Now once iv completed filling the form in, and press save, the ModelState is not valid at this stage, because for some odd reason in the razor view
#Html.HiddenFor(model => model.Colour)
stores the string value "Orange" instead of 23, which has been provided by the model. Now i understand the names clash in the URL and my object model, but surely this should work.
My Route Config
routes.MapRoute(
name: "Item",
url: "{colour}/{controller}/{action}/{id}",
defaults: new { controller = "Item", action = "Index", id = UrlParameter.Optional }
);
My Get method:
[HttpGet]
public ActionResult Create(Colour colour, DateTime? date)
{
var viewModel = new Item()
{
ItemDate = (!date.HasValue ? DateTime.Now.Date : date.Value),
Colour = (int) colour,
ItemTypeId = 1
};
return View(viewModel);
}
My Post Method
[HttpPost]
public ActionResult Create(Item item)
{
if (ModelState.IsValid)
{
_itemService.Create(item);
return RedirectToAction("Index", "Item", new { date = item.itemDate.ToString("yyyy-MM-dd") });
}
return View(item);
}
SO my question is how do i get my razor view to ignore the colour that is written in url and to use colour which is the Item property. So instead of "Orange" it should be 23.
Can anyone advise?
Explicitly excluding it is a good way to go:
public ActionResult YourMethod([Bind(Exclude = "Colour")] Item item{
//Implementation
}
Another option is to explicitly INCLUDE all of the parameters from the request that you want to bind to:
public ActionResult YourMethod([Bind(Include = "NotColour1, NotColour2")] Item item{
//Implementation
}
I tend to prefer this method because
Visual Studio stubs it out this way when it scaffolds stuff (not a technically sufficient reason, but good enough)
Gives me a bit more confidence security-wise, because you say what IS and not what CANT be and I therefore feel just a little bit more in control.
I agree with the suggestion, though, that you should look into choosing different names for non-related things (or at least put them in separate namespaces?)

use id from url directly in a view

Lets say that i have an URL that looks something like this: localhost/userdetails/5 where 5 is the users ID. Is there any way to make use of the ID directly in the view (razor viewengine) and show the details? Or do i handle it in the default action in the controller?
To keep things simple now, focusing on getting the id to the view, you basically want to use the id to populate your model with data and then pass that to the view. So in your controller:
public ActionResult Index(int id)
{
UserModel model = db.Users.Where(u => u.Id == id).SingleOrDefault();
return View(model);
}
The view (very simplified) might look like this:
#model MyProject.Models.UserModel
#Html.DisplayFor(m => m.Id)
#Html.DisplayFor(m => m.Username)
This is very basic though. Eventually, you'll get to a point where you realise you should use viewmodels for your views instead of a domain model that's come directly from the data source. That in itself gives you another problem to solve in the form of mapping properties from the domain model onto your viewmodel. Typically, AutoMapper or ValueInjecter are used for that. For now though, it's probably best to just focus on passing data to a view and getting it back into a controller so that you can do something with it.
Update
This is a simple scenario which demonstrates how to get the data back into the controller. So basically, you'd have a form which you would submit:
#using (Html.BeginForm("Index", "Home"))
{
// Form elements and submit button
}
That would post the data to this action method for you to do whatever you wish with the data:
[HttpPost]
public ActionResult Index(UserModel inputModel)
{
// Check to see if the model's data was valid.
if (ModelState.IsValid)
{
// Do something in the database here.
// Then redirect to give the user some feedback.
return RedirectToAction("Thanks");
}
// The model validation failed so redisplay the view.
return View(inputModel);
}
you can use this in both the controller or in the View as an extension method.
Example: asuming your routes id holder has the default values in global.asax
public int IdFromAdress(HttpContext httpContext)
{
RouteData rd = httpContext.Request.RequestContext.RouteData;
string stringId = (string)rd.Values["id"];
return int.Parse(stringId);
{
You can get the id with this
#HttpContext.Current.Request.RequestContext.RouteData.Values["id"].ToString()
But I would reccomend to use a ViewMdoel to pass the value to the view and not the ViewBag or accessing directly from the view
You should use the model (i.e. the model passed back to your view). A ViewBag is another option but since the ID is part of the model itself, it wouldn't make any sense to do that.
View
#model User
#{
ViewBag.Title = "User Details";
}
#Model.Id;
Controller
public ActionResult UserDetails(int id)
{
return View("UserDetails", (object)id);
}
Yes you can. There is more than one way to do it, but since you've tagged your post MVC, assume you'll want to do it the 'MVC way', which means (imo) using a view model.
So you write a view model
public class MyViewModel()
{
public int ID {get; set;}
}
You populate the model in the controller and pass it to the view
public ActionResut MyView (int id)
{
var viewModel = new MyViewModel {ID = id};
return View (viewModel);
}
Then you have a strongly typed view (strongly typed to the MyViewModel, that is)
and you can reference the model's properties
#Model.ID
Then to make this useful, you can add whatever other properties you're wanting to work with to your view model. Then you can populate them in your controller before rendering the view (to show user info, for example), or let the user populate them for you in the view (using textboxes and such wrapped in a form). Then you can collect the user input in the post action in the controller like so
[HttpPost]
public ActionResult MyView(MyViewModel viewModel)
{
//do stuff with the data from the viewModel
}

Categories

Resources