I am building a wizard - a series of form steps.
Each step of the wizard are submitted to the same action method (it needs to be highly extendable), and all step viewmodels inherit from the same base model. They are bound to their concrete type by a custom model binder. The submit action saves the form fields and returns the next step (or the same in case of errors).
It works well. However, at one point the user needs to supply work information. The step immediately following primary work is secondary work information - same form, same properties. The viewmodel for the second part inherits from the primary viewmodel without implementing any individual properties (the class definition is empty).
After the primary form is submitted, the secondary viewmodel is returned with values reset. However, for some reason, the form is shown with the first viewmodels values.
And the really strange thing is, the model values are overridden by Request.Form values for all EditorFor calls, but not for specific calls:
// This displays correctly, showing the current model values
<div>Model says: #Model.AuthorizationNumber</div>
<div><input type="text" value="#Model.AuthorizationNumber"/></div>
// This displays the Request.Form value from the previous step.
#Html.EditorFor(x => x.AuthorizationNumber)
In the above, the authorization number is shown correctly for the first two lines (constructor values), but the third is incorrect (showing the Request.Form value).
What could be the problem?
EDIT 1
On request, the controller method handling the submit.
public ActionResult Index(StepPathState model = null)
{
var isPreviousSubmit = Request.Form.AllKeys.Contains("submit-previous");
if (model == null || model.Step == null) return View(Steps.GetFirstPathResult());
// submit the step data
var result = Steps.SubmitStepData(model, isPreviousSubmit);
if (result.Status is StepSuccessAndFinish)
{
return Finish(result);
}
var failureStatus = result.Status as StepActionStatusFailure;
if (failureStatus != null)
{
foreach (var error in failureStatus.Errors)
{
ModelState.AddModelError(error.Property, error.ErrorMessage);
}
}
return View(result);
}
I may have found the answer though: http://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx
Solution: The good and the ugly
As the article linked above describes, it seems that MVC favors the modelstate values when returning from a POST. This is because re-displaying the form in a POST should always indicate a validation failure. So the correct solution is to save, redirect to a GET request and display the new form. This would fix the problem, and is what I am going to do when I have time to refactor.
A temporary solution is to clear the modelstate. This will cause it to fallback back to the model. This is my temporary fix:
if (failureStatus == null && ModelState.IsValid)
{
ModelState.Clear();
}
return View(result);
The danger here is that I have cleared the modelstate. And what might that entail in the long run? New bug adventures to come. But I am on the clock.
Related
My MVC5 application is being scanned/spammed, and I'm getting 1,000's of exceptions in my logs. How can I resolve this?
Here's my model:
public class MyModel
{
public bool IsRememberMe { get; set; }
}
Here's my view:
#Html.CheckBoxFor(m => m.IsRememberMe)
Here's my action:
[HtmlPost]
public ActionResult MyAction(MyModel model)
{
if (ModelState.IsValid)
{
// Do work
}
return View(model);
}
When a spammer submits manually, a value such as "IsRememberMe=foo" in the POST, ModelState.IsValid==false, as expected and model.IsRememberMe==false as expected. However, when rendering the resulting view, Html.CheckBoxFor(m => m.IsRememberMe) throws an exception:
System.InvalidOperationException: The parameter conversion from type 'System.String' to type 'System.Boolean' failed.
Inside the controller, if I add:
string value = ModelState["IsRememberMe"].Value.AttemptedValue;
then value equals foo (the input value).
Note that under normal conditions, everything is working correctly.
It looks like Html.CheckBoxFor() is pulling the value from ModelState rather than model.IsRememberMe.
What is the best way to resolve this?
Update
Based on the comments and feedback, it looks like the use of data from ModelState is by design. I'm OK with that under normal circumstances.
Some comments have suggested this is a duplicate. While the solution may be similar, I argue that it is not a duplicate since the problem I am trying to solve is different: I am not actively trying to change the submitted value in my controller.
I am trying to prevent an exception in MVC caused by the submission of invalid data. I am posting my own answer below.
I think this is a bug in the implementation of Html.CheckBoxFor(). Pulling the data from ModelState is fine, however, if the data cannot be converted, then it should fallback to using the value from the model instead of throwing an exception that halts rendering of the view.
I cannot think of a use-case where it's desirable to throw an exception (preventing rendering of the view) if the value in ModelState cannot be converted to a checked/unchecked for a checkbox. I'd be glad to hear from anyone who has such a use-case.
Solution
Due to the simplicity of the data type involved (bool), we can remove the value from ModelState if we are going to re-render the view. This will remove the offending value.
[HtmlPost]
public ActionResult MyAction(MyModel model)
{
if (ModelState.IsValid)
{
// Do work
}
if (ModelState.Keys.Contains("IsRememberMe"))
{
ModelState.Remove("IsRememberMe");
}
return View(model);
}
I am trying to build a object/model in memory that is a representation of what the model would look like if the current ModelState was assumed to be correct and was Merged into the model.
I thought I would look into the source code for TryUpdateModel() or UpdateModel() and came up with the following:
private T GetTempModel<T>(T model, ActionExecutedContext filterContext)
{
var mtype = Type.GetType(model.GetType().FullName);
var m = Activator.CreateInstance(mtype);
var binder = ModelBinders.Binders.GetBinder(mtype);
var bc = new ModelBindingContext()
{
ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => m, mtype),
ModelName = null,
ModelState = filterContext.Controller.ViewData.ModelState,
ValueProvider = filterContext.Controller.ValueProvider
};
var result = binder.BindModel(filterContext.Controller.ControllerContext, bc);
return (T)result;
}
However when I do this the result value is just the default state that was created when I ran the CreateInstance(). It didn't merge all the proposed values in the ModelState.
Am I misunderstanding how this should work? Is there a way to get a model representation of the items in ModelState in object form instead of KeyValuePair?
UPDATE: Additional information per comments.
I am simplifying this a bit but here is the basic scenario for what I am doing. Let's say I have a survey application. The user logs in and is presented with a basic View where they enter their profile information (first, last name etc.) The viewModel used to generate this view has a property, SurveyQuestions, that is initialized with no questions. As they navigate through the next screens they are prompted with questions, these questions are loaded into the View/DOM via AJAX for performance reasons as they answer one question I go grab the next applicable one.
When the user is complete and POSTs the survey I get everything as expected in the ViewModel and in SurveyQuestions I can see all their questions/answer information (List<QuestionBlock>).
However I am using the Post-Redirect-Get (PRG) pattern and if there is an exception during the POST I store the ModelState, redirect, and reapply the ModelState upon exiting the GET. The problem is that in the GET the ViewModel is just the uninitialized and empty question SurveyQuestions property. Because of this the ModelState values aren't initialized properly for the SurveyQuestions when the view is rendered. All other properties get the correct ModelState values when the view is rendered.
This is because I have to initialize the user's SurveyQuestions to have each of the question information before exiting the GET.
This is where I would like to read the values from the ModelState into a temporary object so that I can access them and rebuild those questions server side to correctly rendered in the view.
Using MVC5, I have a model with an int value that is not nullable and should not be nullable, however I do not want the model binder to automatically require that field (the view is bound to a List<> and any one item in the List may be left empty and therefore not saved to db) so I am setting the following in Application_Start():
DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
This works fine when my view is initially displayed, but when the user clicks Save, all validation passes, the Controller Action then does additional cross-record validation (total across records must equal 100 for instance). If that validation fails, I then set a custom error and return the view:
ModelState.AddModelError("", "Total Allocation must equal 100%.");
return View("Shipment", shipment);
Once that total amount is corrected, the user again clicks Save, and now the implicit Required validation starts occurring on the client-side and never sends the HttpPost back to the Controller correction: the implicit validation occurs on the server side, not client side. That's the same implicit validation I turned off in Application_Start (I checked in debug mode and it's still false when the View returns with the error).
Is this a bug with AddImplicitRequiredAttributeForValueTypes or am I doing something wrong?
TIA
-VG
I found a work-around, which I wouldn't call a "resolution" but at least it is a work-around in case someone else finds this helpful. At first I thought the implicit validation was happening client-side, but no, it does make it into the Controller but ModelState.IsValid is now false. This is important because my work-around relies on control being sent to the controller. Here is what I did:
//Remove implicit validations
foreach (KeyValuePair<string,ModelState> stateItem in ModelState)
{
if (stateItem.Key.Contains("AllocationAmount"))
{
if (stateItem.Value.Errors.Count > 0 && stateItem.Value.Errors[0].ErrorMessage.Contains("required"))
{
stateItem.Value.Errors.RemoveAt(0);
}
}
}
//Check Validation
if (!ModelState.IsValid)
{
return PartialView("pvShipment", shipment);
}
By checking for that specific column having a "required" error message and removing it before checking ModelState.IsValid, the IsValid will now return true (as long as no other errors exist of course), and is able to continue on with the save logic. I hope this makes sense.
-VG
I have a form like ...
#using (Html.BeginForm("create", "Account", FormMethod.Post, new { id = "accountform_form" }))
{
#Html.TextBoxFor(e => e.ShipFirstName)
...
}
while testing, I was surprised to see the field retained its value on postback even without me assigning it to the view-model. Using the debugger, the value for ShipFirstName is null right at the end of the action when returning the view, so why would it show the value that was in the field? Have I been unnecessarily assigning posted values to view-model properties all this time? Or is there something else going on?
Update: the action is like so...
[HttpPost]
public ViewResult Create(AccountFormModel postModel)
{
var model = new AccountFormModel(postModel, stuff, stuff); //I use posted values and paramters to create the actual view model
return view(model);
}
So, I see the GET form, enter values, say I enter a field and leave a required field blank, submit, the resulting page has the value I entered in the other field, who's putting it there when in the model it's null?
I ran into something similar earlier today (a checkbox was always checked). Have a look at Changing ViewModel properties in POST action and see if this is similar.
Basically calling ModelState.Clear() fixed it for me.
As you're passing the model back to the view after it has been POSTed, MVC is taking the stance that you're doing so because the form contains errors. So, rather than making the user fill out the form again, it repopulates it using the ModelState collection. In this case, the values in the ModelState collection take precedence over the changes you make in the action (which does feel a bit weird).
You can get around this either by calling ModelState.Clear() or using ModelState.Remove(string key), where key is the name of the property.
If you'd like a full explanation of why this is the case, see ASP.NET MVC’s Html Helpers Render the Wrong Value!. Excerpt:
Why?
ASP.NET MVC assumes that if you’re rendering a View in response to an HTTP POST, and you’re using the Html Helpers, then you are most likely to be redisplaying a form that has failed validation. Therefore, the Html Helpers actually check in ModelState for the value to display in a field before they look in the Model. This enables them to redisplay erroneous data that was entered by the user, and a matching error message if needed.
Alright, I lost the entire morning on this and I'm getting nowhere.
I have a good experience with MVC, been using it since the first betas back in 2008 but I can' t really figure this out.
I substantially have 2 GET methods: the first one renders a form. The second one is the method the form points to. I'm submitting using GET because it's a search form, and I want to have a bookmarkable URL with the parameters.
Something like this
[HttpGet]
public ActionResult DisplayForm()
{
Contract.Ensures(Contract.Result<ActionResult>() != null);
Contract.Ensures(Contract.Result<ActionResult>() is ViewResult);
return this.View();
}
[HttpGet]
public ActionResult Search(MyViewModel viewModel)
{
Contract.Requires<ArgumentNullException>(viewModel != null);
Contract.Ensures(Contract.Result<ActionResult>() != null);
Contract.Ensures(Contract.Result<ActionResult>() is ViewResult || Contract.Result<ActionResult>() is RedirectToRouteResult);
var result = this.validator.Validate(viewModel); //FluentValidation validation
if (!result.IsValid)
{
result.FillModelState(this.ModelState); //extension method that uses AddModelError underneath for ValidationMessageFor helpers on search form
return this.RedirectToAction(c => c.DisplayForm()); //MvcContrib redirect to action
}
ViewData.Model = viewModel;
return View();
}
The first time I submit the form, the viewData in the target method gets populated correctly.
If I go back and do another search, it's like the modelbinder "cached" the data I submitted the first time: the viewData always has the values from the first search. It resets only restarting the application.
I tried checking both ModelState and HttpContext.Request and they DO have the new data in them (not stale) but still the viewData gets populated with old data. I also tried overriding OnActionExecuting and OnActionExecuted simply to put a breakpoint in there and check the ModelState in those steps, and found nothing weird.
I also tried to call the Search method directly via the browser bar, since it's in GET and I can do it. Still, ModelState and Request contain the data I input, but the viewData has old data.
This is really getting to my nerves as I can't really understand what's going on.
Any help would really be appreciated.
Have you tried
ModelState.Clear()
When you're done with the search call?
I experimented a lot and I found out that the problem was on an actionfilter on a base class which I was unaware of. The "PassParametersDurignRedirect" filter of MvcContrib. Without it everything works fine.