How does the ModelStateDictionary in ASP.NET know which model to validate? - c#

Take this bit of generated code for example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Bill bill)
{
if (ModelState.IsValid)
{
db.Entry(bill).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(bill);
}
There is a Model called "Bill", that has some [Required] attributes set for it.
My question is: how does the ModelState.IsValid know that it should be checking the Bill entity as opposed to another entity?

There's a default model binder in ASP.NET MVC called DefaultModelBinder. This class will automatically execute for each of the action parameters you defined. And when it attempts to instantiate and populate the corresponding model from the request key/value string pairs, it might encounter errors that this model binder simply adds to the ModelState dictionary. The reason why it might encounter errors is because you might have decorated your model with validation attributes.
So once the code execution enters the controller action, the ModelState.IsValid property will return false if there are errors added to it during model binding.
By the way your code is equivalent to the following (never to be used, just for illustration purposes):
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit()
{
Bill bill = new Bill();
if (TryUpdateModel(bill))
{
db.Entry(bill).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(bill);
}

Related

ModelState.IsValid = False

ModelState is throwing an error that one of the parameters [UserId] is null. That field isn't being set on my form. It's being set in the controller.
How do I remove it from the ModelState.IsValid test?
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,UserId,Created,TimeStamp,Name,Role,Description,Partner,PartnerAmount,Competitor,IsDeleted")] Relationship relationship)
{
relationship.UserId = User.Identity.Name.Replace(#"XXXXX\","");
relationship.Created = DateTime.Now;
relationship.TimeStamp = DateTime.Now;
relationship.IsDeleted = false;
if (ModelState.IsValid)
{
_context.Add(relationship);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(relationship);
}
It won't pass ModelState.IsValid b/c of UserId. But, it doesn't check Created, Timestamp or IsDeleted. Why doesn't it check those, but it does check UserId? All of these fields are required in my Model.
Remove the field from the view model. If you don't have one, create on. Using entities in your views generates security vurnabilities as an attacker could set fields that you might not change once posted.
Using an view model makes sure that only fields that are allowed to be changed are available in the browser.
If you still want to use your entity, change the UserId to int? which tells the model validator that it doesn't have to be set.
You can also remove the validations ModelState["UserId"].Errors.Clear(); But I strongly discourage from that. Use a specific view model.
You can't change ModelState.IsValid by changing any properties inside of the controller. IsValid still will be the same, since a list of errors will be the same. UserId should be assigned before the model reaches the controller.
So assign UserId=0 if it is nullable and add this line somewhere inside of the form of the view,
<input type="hidden" asp-for="#Model.UserId" value="#Model.UserId"/>
In the controller action method, try to use Rerun validation, call the TryValidateModel method, as shown here:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,UserId,Created,TimeStamp,Name,Role,Description,Partner,PartnerAmount,Competitor,IsDeleted")] Relationship relationship)
{
relationship.UserId = User.Identity.Name.Replace(#"XXXXX\","");
relationship.Created = DateTime.Now;
relationship.TimeStamp = DateTime.Now;
relationship.IsDeleted = false;
if (!TryValidateModel(relationship, nameof(relationship)))
{
return View(relationship);
}
_context.Add(relationship);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}

What happening behind scene when we want to render view?

ActionResult is the base class for the various return types to View in MVC. So your action must return an ActionResult or a class derived from it in order to work.
so we can use
public ContentResult Index()
{
return Content("Hello world");
}
or for example
public ViewResult Index()
{
return View();
}
or ActionResult
public ActionResult Index()
{
if (ViewBag.Hello = "World")
return Json("");
return PartialView();
}
BUT also is possible use string !!!
public string Index()
{
return "Hello World";
}
WHY is than not possible return integer to view? (Or maybe it is?)
public int Index()
{
return 4;
}
and not possible return some entity to view (Or maybe it is?)
public User Index()
{
return new User();
}
My question is : What happening behind scene when we want to render view?
I agree that this is quite a broad question, but I wanted address and answer a few of the points you raised in your question.
You can return an int, string or object from your action method and it will simply return the object's string representation as the result.
Therefore you don't have to return an object of type ActionResult in order for it to work, but the ActionResult enables useful functionality through it's various implementations so that ASP.NET MVC Framework can handle different scenarios straight out of the box.
Such as returning views and handling the ViewModel you want to pass to your view:
return View(); // Default view without view model
return View(viewModelObject); // Default view with a view model
Returning views based on your routing information:
return View("viewName", viewModelObject);
Performing redirects to another page, using your routing information:
return RedirectToAction(actionName, controllerName);
Returning a page with specific status codes:
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
Returning JSON instead of a view:
return JsonResult(myObject);
All of the above examples do different things, return different types of results and handle your objects for you so that you don't have to code the behaviour yourself - they're ready for you to use.
Another handy thing is that you can create your own implementations of ActionResult to create your own behaviour, so it's very flexible in that regard.
I agree with #Daniel J.G. that you should do some more reading on how ASP.NET MVC hangs together and it will become a lot more clear to you.

Storing Model in Session VS Recreating It

Is there any danger to use one of the 2 following methods? Is there any best practice?
I'm wondering if I will hit a wall when there will be many user online at the same time on the website.
Storing Model in Session for later reuse
public ActionResult Index()
{
var model = new Model();
Session["Model"] = model;
return View(model);
}
[HttpPost]
public ActionResult Index(FormCollection fc)
{
var model = (Model)Session["Model"];
//Update stuff
return View(model);
}
Recreating it on every requests
public ActionResult Index()
{
var model = new Model();
return View(model);
}
[HttpPost]
public ActionResult Index(FormCollection fc)
{
var model = new Model();
//Update stuff
return View(model);
}
Each model :
might contain a lot of calls to the database (it can take some time to
initialize them)
might contain list of information (a lot of data)
Edit
Feels like most avoid trying to answer the question and got stuck on unimportant details to the question.
I accepted the only answer which was able to stay on topic.
Atavari answer is completely off topic and didn't understood the question at all.
Both of those methods are wrong. Your post action should receive the model, not a FormCollection object.
[HttpPost]
public ActionResult Index(Model model)
{
//Update stuff
return View(model);
}
And, you usually redirect to a different page after the Add/Update is successful, and only return the same View if there's validation error. So, your post action should look more like this:
[HttpPost]
public ActionResult Index(Model model)
{
if(ModelState.IsValid)
{
//Update stuff
//return to a different page or whatever needs to be done
//after a successful update
}
// If model is not valid...
return View(model);
}
UPDATE:
If there are properties in your model that are not posted to your action (like a SelectList for a dropdown), you'll only need them if you want to return the same View (normally when there's validation error). In that case you don't have to recreate your model. You just repopulate those properties.
Let's say your model has a property called Items. This is how your actions should look like:
public ActionResult Index()
{
var model = new Model();
model.Items = GetItems();
return View(model);
}
[HttpPost]
public ActionResult Index(Model model)
{
if(ModelState.IsValid)
{
//Update stuff
//return to a different page or whatever needs to be done
//after a successful update
}
// If model is not valid...
model.Items = GetItems();
return View(model);
}
It depends on how many users you really have. Using session can be comfortable and easy but scalability may become an issue.
If you use it just to improve performance, I would suggest you to use cache instead. Take care that objects in cache can expire so you have always to check whether they are still there. Cache is better than session because it is self optimized. If memory gets low it's automatically cleaned, while session variables are never cleaned (unless of course when session expires).

Handling Http Post and Get in the same view - ASP.Net MVC Razor

My view has 2 label 2 text-boxes and a button.
This is my controller :
public ActionResult Create()
{
// Custom model that holds values
// this is to set the default values to the text boxes
return View(model);
}
[HttpPost]
public ActionResult Create(CustomModel viewModel )
{
try
{
// TODO: Add insert logic here
// The button should trigger this method to perform update
return RedirectToAction("Create");
}
catch
{
return View();
}
}
When I run the program it automatically goes to my model which contains no value and throws a null pointer exception. Any ideas on how to preserve the model state. ?
Update :
I am using classic ADO.Net Model not the entity framework.
The http get and the post method follows a different logic. The Model with values are returned using the get method and the state of this model needs to be preserved for updating the corresponding records to the database using the post method.
But, when I use the post method, the compiler automatically routes to my model class looking for a parameter less constructor. I believe it is creating a new instance of my model class.
Not entirely sure I follow, but you can return the same view from your POST action, passing in the model. This will preserve the model data.
[HttpPost]
public ActionResult Create(CustomModel viewModel )
{
try
{
// TODO: Add insert logic here
// The button should trigger this method to perform update
// Return "Create" view, with the posted model
return View(model);
}
catch
{
// Do something useful to handle exceptions here.
// Maybe add meaningful message to ViewData to inform user insert has failed?
return View(model);
}
}
Well you can put validation in your view and then use ModelState.IsValid property as shown in below code :-
[HttpPost]
public ActionResult Create(CustomModel viewModel )
{
if (ModelState.IsValid)
{
////Insert logic here
return RedirectToAction("Create");
}
return View(viewModel);
}
Your "Get" method is returning a model that isn't present in your code(from what you have shown here). Below is a way to have your Action accept GET & POST.
[AcceptVerbs(HttpVerbs.Get|HttpVerbs.Post)]
public ActionResult Create(CustomModel viewModel)
{
try
{
// TODO: Add insert logic here
// The button should trigger this method to perform update
return RedirectToAction("Create");
}
catch
{
return View();
}
}
You have to create new instance in your Action Result method.
public ActionResult Create()
{
// Assuming - First Time this ActioResult will be called.
// After your other operations
CustomModel model = new CustomModel();
// Fill if any Data is needed
return View(model);
// OR - return new instance model here
return View(new CustomModel());
}

ASP.Net MVC Object Reference in Edit View when using DropDownListFor()

This question is related to another I ask recently, it can be found here for some background information.
Here is the code in the Edit ActionResult:
public virtual ActionResult Edit(int id)
{
///Set data for DropDownLists.
ViewData["MethodList"] = tr.ListMethods();
ViewData["GenderList"] = tr.ListGenders();
ViewData["FocusAreaList"] = tr.ListFocusAreas();
ViewData["SiteList"] = tr.ListSites();
ViewData["TypeList"] = tr.ListTalkbackTypes();
ViewData["CategoryList"] = tr.ListCategories();
return View(tr.GetTalkback(id));
}
I add lists to the ViewData to use in the dropdownlists, these are all IEnumerable and are all returning values.
GetTalkback() returns an Entity framework object of type Talkback which is generated from the Talkback table.
The DropDownListFor code is:
<%: Html.DropDownListFor(model=>model.method_id,new SelectList(ViewData["MethodList"] as IEnumerable<SelectListItem>,"Value","Text",Model.method_id)) %>
The record I am viewing has values in all fields. When I click submit on the View, I get an Object reference not set to an instance of an object. error on the above line.
There are a number of standard fields in the form prior to this, so the error is only occurring on dropdown lists, and it is occurring on all of them.
Any ideas? This is my first foray in to MVC, C#, and Entity so I am completely lost!
If you have [HttpPost] method like that
[HttpPost]
public ActionResult Edit(Talkback model)
{
//Do something with model
return View(model);
}
You have to fill ViewData again. If you don't do it, you'll have Object reference not set to an instance of an object errors.
The best thing to do would be to follow POST-REDIRECT-GET patter and do it like that:
[HttpPost]
public ActionResult Edit(Talkback model)
{
//Do something with model
return RedirectToAction("Edit", new { id = model.id });
}
You'll have ViewData filled again by [HttpGet] method.

Categories