Updating my model then re-evaluate IsValid? - asp.net-mvc

I'm passing in some values to my controller action and everything is binding up fine. There will be two properties missing from the form POST by design.
I am then setting the missing values but then I want to validate the model and it is still saying false since it looks like the ModelState hasn't caught up with my changes.
[HttpPost, Authorize]
public ActionResult Thread(int id, string groupSlug, Comment comment, string submitButton)
{
comment.UserID = UserService.UID;
comment.IP = Request.UserHostAddress;
UpdateModel(comment); //throws invalidoperationexception
if (ModelState.IsValid) // returns false if i skip last line
{
//save and stuff
//redirect
}
//return view
}
What is the cleanest way to pat the ModelState on the head and tell it that everything will be okay whilst still validating everything else that was bound from the user's POST

If the missing Values are required for your model but will not be provided until after binding you may need to clear the errors caused by those two values from the ModelState.
[HttpPost, Authorize]
public ActionResult Thread(int id, string groupSlug, Comment comment, string submitButton)
{
comment.UserID = UserService.UID;
comment.IP = Request.UserHostAddress;
//add these two lines
ModelState["comment.UserID"].Errors.Clear();
ModelState["comment.IP"].Errors.Clear();
UpdateModel(comment); //throws invalidoperationexception
if (ModelState.IsValid) // returns false if i skip last line
{
//save and stuff
//redirect
}
//return view
}

I'm using the ASP.NET Core 1.0.0 and async binding and for me the solution was to use ModelState.Remove and pass the property name (without object name).
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit([Bind("AerodromeID,ObservationTimestamp,RawObservation")] WeatherObservation weatherObservation)
{
weatherObservation.SubmitterID = this.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
weatherObservation.RecordTimestamp = DateTime.Now;
ModelState.Remove("SubmitterID");
if (ModelState.IsValid)
{
_context.Add(weatherObservation);
await _context.SaveChangesAsync();
return RedirectToAction("Index", "Aerodrome");
}
return View(weatherObservation);
}

Before .NET Core you can use Validate(TEntity entity) function of controller. But first you have to clear existing ModelState errors.
Suppose you set a missing required property to entity.
ModelState.Clear();
Validate(entity);
if (!ModelState.IsValid) {}
With Core you can use TryValidateModel instead of ModelState.IsValid
see thiis: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0

Related

Basic wizard, and passing model from one view to another?

I'm new to MVC and trying to create a wizard-style series of views, passing the same model instance from one view to the next, where the user completes a little more information on each form. The controller looks something like this:-
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return View("Step2", model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return View("Step3", model);
}
// etc..
Questions:-
When I submit the form from the Step1 view, it calls the Step1 POST method and results in the Step2 view being displayed in the browser. When I submit the form on this view, it calls the Step1 POST method again! I got it to work by specifying the action and controller name in Html.BeginForm(), so I'm guessing that the parameterless overload just POSTs back to the action that rendered the view?
I've noticed that the browser's address bar is out of sync with the current view - when I'm on the Step2 view it still shows the Step1 URL, and when on Step3 it shows the Step2 URL. What's going on?
Another approach I've seen for passing a model between views is to put the model in TempData then use RedirectToAction(). What are the pros and cons of this method versus what I'm currently doing?
I won't be providing any "back" buttons of my own in the wizard. Are there any pitfalls to be aware of regarding the browser's back button, and do either of the above two approaches help (or hinder)?
Edit
Prompted by #StephenMuecke's comment I've now rewritten this to use a single view. I tried this once before but had difficulties round-tripping a "step number" to keep track of where I was in the wizard. I was originally using a hidden field created with #Html.HiddenFor', but this wasn't updating as the underlying model property changed. This appears to be "by design", and the workaround is to create the hidden field using vanilla HTML (
Anyway the one-view wizard is now working. The only problem is the old chestnut of the user being able to click the back button after they have completed the wizard, make a change, and resubmit a second time (resulting in a second DB record).
I've tried adding [OutputCache(NoStore = true, Duration = 0, VaryByParam = "None")] to my POST method, but all this does is display (in my case) a Chrome error page suggesting that the user clicks refresh to resubmit the form. This isn't user friendly and doesn't prevent a second submit.
you can use RedirectToAction() in this case without worrying about TempData. Just add your model as a parameter to each action and use RedirectToAction("Step2", model);
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Step2", model);
}
[HttpGet]
public ActionResult Step2(MyModel model)
{
return View(model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Step3", model);
}
// etc..
The answer to #1 is found in #2.. if you dont specify the Action in you Html.BeginForm() it posts to the current url.
Using TempData to avoid model displaying in url.
[HttpGet]
public ActionResult Step1()
{
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
TempData["myModel"] = model;
return RedirectToAction("Step2");
}
[HttpGet]
public ActionResult Step2()
{
var model = TempData["myModel"] as MyModel;
return View(model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
return View(model);
TempData["myModel"] = model;
return RedirectToAction("Step3");
}
// etc..
Another option would be to add the name of the next action to ViewBag and set your actionName in each BeginForm()
[HttpGet]
public ActionResult Step1()
{
ViewBag.NextStep = "Step1";
return View();
}
[HttpPost]
public ActionResult Step1(MyModel model)
{
if (!ModelState.IsValid)
{
ViewBag.NextStep = "Step1";
return View(model);
}
ViewBag.NextStep = "Step2";
return View("Step2", model);
}
[HttpPost]
public ActionResult Step2(MyModel model)
{
if (!ModelState.IsValid)
{
ViewBag.NextStep = "Step2";
return View(model);
}
ViewBag.NextStep = "Step3";
return View("Step3", model);
}
//View
#using (Html.BeginForm((string)ViewBag.NextStep, "ControllerName"))
{
}
I'd prefer to add NextStep as a property to MyModel and using that instead of using ViewBag though.
I understand the thought behind your approach and don't have any issues with it. Unfortunately, I don't believe that ASP.NET MVC is geared very well for passing the the same view model (with data!) between different actions.
Typically, the scaffolded actions in the controller will either create a model item or find it by identifier in the database.
I don't know if this would help, but you could try to save it to the database on every step, and then retrieve it by identifier, or you could also save it to a session and grab it that way.
One issue I do see with your approach is you have Step2 set as a get, yet you probably want to post data to it from Step1 instead of using a query string. You may need to reconcile that issue.

modify field value in post back not working

I have the following snippet of asp.net mvc controller code that check if state is invalid, then I will update the value of one field:
[HttpPost]
public ActionResult Create(ContactInfo contactinfo)
{
if (IsModelStateValid(GetIssues(contactinfo)))
{
db.ContactInfoes.Add(contactinfo);
db.SaveChanges();
return RedirectToAction("Index");
}
contactinfo.Name+="why this is not working".
return View(contactinfo);
}
Through debugging I checked that the new value of Name field is successfully passed to Model of my View, but in the render result, only the field validation fields are updated, the field value change is not rendered, Could someone help me on how to apply this change?
You got sort of cache problem, clear it with:
[HttpPost]
public ActionResult Create(ContactInfo contactinfo)
{
if (IsModelStateValid(GetIssues(contactinfo)))
{
db.ContactInfoes.Add(contactinfo);
db.SaveChanges();
return RedirectToAction("Index");
}
// Clear the model state.
ModelState.Clear(); // <-----------------------------------------------
// Or just remove the `Name` property:
ModelState.Remove("Name")
contactinfo.Name+="why this is not working".
return View(contactinfo);
}

mvc c# return view() not defaulting to correct values

I am not sure if I am overlooking something obvious.
Once I do a POST, I have the following (Note: What I am trying to do is to default the same view with some null values so the user can create another entry):
[HttpPost]
public ActionResult QkInsert(ProgInfo model)
{
if (ModelState.IsValid)
{
ProgService.InsertQuickEntry(model);
model.Name = null;
model.Address = null;
model.Phone = null;
return view(model);
}
return view(model);
What is strange is that when I look at the value of model in the IsValid()
return view(model)
I do see the null values.
But when it is shown on the view, it is not null. It is basically the same view as when I had entered the data the first time. Any idea? Have I overlooked something?
Also notice how I have done return view(model) twice. Is there any other way of doing this to where I do it only once and not repeat?
That's because HTML helpers are first looking into the ModelState when binding their values and only after that the value in your model. This is by design.
So if you want to change any value of the model inside a POST action you need to remove it from the ModelState first:
[HttpPost]
public ActionResult QkInsert(ProgInfo model)
{
if (ModelState.IsValid)
{
ProgService.InsertQuickEntry(model);
ModelState.Remove("Name");
ModelState.Remove("Address");
ModelState.Remove("Phone");
model.Name = null;
model.Address = null;
model.Phone = null;
return view(model);
}
....
}
Now the view will render the modified values.
If the model is not valid you will return the same model, your second return.
There is no need at all for the first return view(model) as there is no code between it and the second one, so it will call anyway. That is, delete the first return and the logic is identical.

MVC 3 RTM allowHtml doesn't work when using FormCollection

MVC 3 RTM. Have a model that has an attribute with AllowHtml. In my controller action, if the action has FormCollection as parameter, it throws the exception:
[HttpPost]
public ActionResult Edit(FormCollection collection, int id)
{
var myEntity = _myRepo.Get(id);
TryUpdateModel(myEntity);
return DoSave(myEntity);
}
A potentially dangerous Request.Form
value was detected from the client
However if my controller action uses an object instead of FormCollection it doesn't throw the exception.
[HttpPost]
public ActionResult Edit(MyEntity postedEntity, int id)
{
var myEntity = _myRepo.Get(id);
TryUpdateModel(myEntity);
return DoSave(myEntity);
}
I've already setup
httpRuntime
requestValidationMode="2.0"
Why does it fail when using FormCollection?
You can't use AllowHtml with FormCollection. You could use the [ValidateInput] attribute but obviously this disabled validation for all values:
[HttpPost]
[ValidateInput(false)]
public ActionResult Edit(FormCollection collection, int id)
{
var myEntity = _myRepo.Get(id);
TryUpdateModel(objective);
return DoSave(objective);
}
This being said I would use the following:
[HttpPost]
public ActionResult Edit(MyEntity entity)
{
if (ModelState.IsValid)
{
_myRepo.Save(entity);
return RedirectToAction("Success");
}
return View(entity);
}
For security-reasons, simply disabling validation is not a good solution, as you're inadvertently disabling security for that action-method entirely.
When you need just one GET or POST value, this is extremely annoying - for example, Request.Params["xyz"] will throw if there's an HTML-value anywhere in your GET/POST data, even if the "xyz" value you posted does not contain HTML.
(This is true as of the latest MVC 3.1 release.)
To get around this issue, I extended my Controller base-class with the following method:
/// <summary>
/// Gets a value from the current Controller's ValueProvider, bypassing post-data validation.
/// </summary>
public string GetUnvalidatedValue(string key)
{
ValueProviderResult result;
if (ValueProvider is IUnvalidatedValueProvider)
{
result = ((IUnvalidatedValueProvider)ValueProvider)
.GetValue(key, skipValidation: true);
}
else
{
result = ValueProvider.GetValue(key);
}
return result == null ? null : result.AttemptedValue;
}
This effectively allows you to get an individual GET/POST value while bypassing the validation.
I believe this is better, safer and more correct than turning off validation altogether - your application does benefit from the added security, even if the way it's implemented gets in the way, and apparently is pretty painful to get around.
(I don't think this is by design, or at least not by very good design...)

Why is ModelStateToTempData Attribute from MVCContrib not working?

I'm simply trying to pass the ModelState from one action to another in the same controller, for validation purposes. However, the model state does not get updated. I see that TempData["____MvcContrib_ValidationFailures____"] contains the ModelStateDictionary from the forwarding Action, but I assumed this should get transfered into my current ModelState automatically? Where am I going wrong?
I'm using ASP.NET MVC2 and MVCContrib 2.0.36.0. I have also tried decorating the Controller with this attribute, but the results are the same.
Code:
[HttpGet]
[ModelStateToTempData]
public ActionResult NewsEventsSignup()
{
var newsEventsSignupDetails = this.TempData.GetItem<NewsEventsSignupDetails>();
var viewModel = _newsEventsSignupPageViewModelMapper.MapFrom(newsEventsSignupDetails);
return this.View(viewModel);
}
[HttpPost]
[ModelStateToTempData]
[ValidateAntiForgeryToken]
public ActionResult NewsEventsSignup(NewsEventsSignupFormViewModel newsEventsSignup)
{
ActionResult resultToReturn;
var newsEventsSignupDetails = _newsEventsSignupDetailsMapper.MapFrom(newsEventsSignup);
try
{
_newsEventsSignupTasks.SignupForNewsAndEvents(newsEventsSignupDetails);
resultToReturn = this.RedirectToAction(x => x.Index());
}
catch (RulesException e)
{
e.AddModelStateErrors(this.ModelState); // from xVal
this.TempData.AddItem(newsEventsSignupDetails); // for showing invalid input
resultToReturn = this.RedirectToAction(x => x.NewsEventsSignup());
}
return resultToReturn;
}
How do you check that ModelState is not filled? This is an OnActionExecuted filter, so it is only filled, when the Action finished. You can not check the value in the action.
The easiest way to validate that an ModelState realy has an error is to put a validation summary on the view.
To see that your error is not xval related I would try
ModelState.AddModelError("TestError", "This is an errortest");
In NewsEventsSignup before redirecting.
Also dont try to acces TempData in the debugger or in some debug-code. It is deleted the first time you access it.

Resources