This is a follow on from the following question:
MVC 3 + $.ajax - response seems to be caching output from partial view
There is a detailed description of the problem over there. However, I have now managed to narrow down the problem, that seems to be with the Html.EditorFor helpers, hence the new question.
The issue:
I post data to the server using $.ajax, then return the html of the partial view that holds the input controls. The problem is that, despite passing a newly created object to the Partial Views model, the various #Html.EditorFor and #Html.DropDownListFor helpers return the OLD DATA!.
I can prove that the model has correctly passed in a new object to the helpers, by printing the value out beside the Html helper. Ie:
#Html.EditorFor(model => model.Transaction.TransactionDate)
#Model.Transaction.TransactionDate.ToString()
As the following image shows, the #Html.EditorFor is returning the wrong data:
[Note that the value beside the Comentario text box is a date time, because I was testing replacing the default values with a value that would change with each post, ie, a DateTime.]
If I replace the #Html.EditorFor for TransactionDate with a plain old #Html.TextBox():
#Html.TextBox("Transaction_TransactionDate", Model.Transaction.TransactionDate)
Then it renders the correct TransactionDate value for a new Transaction object, ie, DateTime.MinValue (01/01/0001...).
Therefore...
The problem is with the #Html.EditorFor helpers. The problem also happens with TextBoxFor and DropDownListFor.
The problem being that these helpers seem to cache the old value.
What am I doing wrong??!
EDIT:
I have just tried debugging in the custom Editor template for dates, and in there, ViewData.TemplateInfo.FormattedModelValue shows the correct value, ie, "01/01/0001". However, once it gets to Fiddler, the response is showing the old date, eg, "01/09/2011" in the image above.
As a result, I just think that there is some caching going on here, but I have none set up, so nothing makes any sense.
There is no caching involved here. It's just how HTML helper work. They first look at the ModelState when binding their values and then in the model. So if you intend to modify any of the POSTed values inside your controller action make sure you remove them from the model state first:
[HttpPost]
public virtual ActionResult AjaxCreate(Transaction transaction)
{
if (ModelState.IsValid)
{
service.InsertOrUpdate(transaction);
service.Save();
}
service.ChosenCostCentreId = transaction.IdCostCentre;
TransactionViewModel viewModel = new TransactionViewModel();
ModelState.Remove("Transaction");
viewModel.Transaction = new Transaction();
ModelState.Remove("CostCentre");
viewModel.CostCentre = service.ChosenCostCentre;
...
return PartialView("_Create", viewModel);
}
Even if you do not specify caching, it sometimes can occur. For my controllers which handle AJAX and JSON requests, I decorate them as follows:
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
This specifically declares no caching should occur.
UPDATE
Based on an answer Darin Dimitrov gave here, try adding the following line to your controller action:
ModelState.Clear();
i have never seen this but basically if you are using ajax to request this data, you need to set nochache: i am assuming you using jQuery.ajax here so will show the code:
$.ajax({
url: "somecontroller/someAction,
cache: false, // this is key to make sure JQUERY does not cache your request
success: function( data ) {
alert( data );
}
});
just a stab in the dark, i assume you have probably already covered this already. have you tried to create a new model first and then populate that new instance of the model with your data, and then send this to your view!
Finally not sure what DB server your using but have you check to see that DB results are not cached and that you are not just requesting SQL results from the DB cache... i dont use MsSQL but i hear that it has outputCaching until something is change on the DB server itself?? anyway just a few thoughts
This was unexpected behavior for me, and although I understand the reason why it's necessary to give ModelState precedence, I needed a way to remove that entry so that the value from Model is used instead.
Here are a couple methods I came up with to assist with this. The RemoveStateFor method will take a ModelStateDictionary, a Model, and an expression for the desired property, and remove it.
HiddenForModel can be used in your View to create a hidden input field using only the value from the Model, by first removing its ModelState entry. (This could easily be expanded for the other helper extension methods).
/// <summary>
/// Returns a hidden input field for the specified property. The corresponding value will first be removed from
/// the ModelState to ensure that the current Model value is shown.
/// </summary>
public static MvcHtmlString HiddenForModel<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression)
{
RemoveStateFor(helper.ViewData.ModelState, helper.ViewData.Model, expression);
return helper.HiddenFor(expression);
}
/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model. Call this when changing
/// Model values on the server after a postback, to prevent ModelState entries from taking precedence.
/// </summary>
public static void RemoveStateFor<TModel, TProperty>(this ModelStateDictionary modelState, TModel model,
Expression<Func<TModel, TProperty>> expression)
{
var key = ExpressionHelper.GetExpressionText(expression);
modelState.Remove(key);
}
Call from a controller like this:
ModelState.RemoveStateFor(model, m => m.MySubProperty.MySubValue);
or from a view like this:
#Html.HiddenForModel(m => m.MySubProperty.MySubValue)
It uses System.Web.Mvc.ExpressionHelper to get the name of the ModelState property. This is especially useful when you have "Nested" models since the key name isn't obvious.
Make sure you're not doing this:
#Html.EditorFor(model => model.Transaction.TransactionDate.Date)
I did this, and the model never got the value back. It worked perfectly once I remove the .Date.
Related
Doesn't seem like this should work so I have no idea why it does. I LIKE the result, but worried I can't depend on it because I have no idea how it is working.
[HttpGet]
public ActionResult Modify(System.Guid id)
{
return View("Modify", LoadFromDatabase(id));
}
[HttpPost]
public ActionResult Modify(CaseModel myModel)
{
//So odd behavior. If I redirect to the actual GET Modify,
//I loose any changes on the form. however if I perform the
//same actions but here in the Post... all my unsaved changes
//stick..why?
//This one wipes any edits
return RedirectToAction("Modify", new { id = myModel.ID});
//This one actually leaves all my changes, even though
//I am re-creating the model from the database just like
//the other ActionResult
return View("Modify", LoadFromDatabase(myModel.ID));
}
As Stephen Muecke mentions in the two comments above,
You can depend on it. RedirectToAction() is redirecting to a new page which initializes a new instance of CaseModel based on ID property (i.e. reads the values from your repository. return View() is returning the current instance of CaseModel and if you using the HtmlHelper methods to generate form controls, it will use the values from ModelState which are added by the DefaultModelBinder when you submit the form. Refer this answer for more explanation of the behavior). #Html.TextBoxFor(m => m.someProperty) will display the value you posted, whereas #Model.someProperty will display the 'updated' value you set in the post method
works perfectly. The HTML helpers overwrite my Model values with the ModelState (aka ViewState).
I have a controller action UpdateCustomer(CustomerDto customer) that returns a PartialViewResult with a model that is also a CustomerDto:
[HttpPost]
public PartialViewResult UpdateCustomer(CustomerDto customer)
{
CustomerDto updatedCustomer = _customerService.UpdateCustomer(customer);
updatedCustomer.Name = "NotThePostedName";
return PartialView("CustomerData", updatedCustomer);
}
In my view, I have the following line:
#Html.TextBoxFor(model => model.Name)
So far, so good. In my view I do an asynchronous post to this action method, the model binder does its work and I can update a customer in the database. Then I want to render the updated customer to the client. For example, I'd like to change the customer name in my controller. However, what gets rendered is always the properties from the posted customer, not the properties from updatedCustomer.
I decided to include the MVC3 source code in my project to see what really happens. It appears to be a feature (bug?) of MVC3 that it always takes the value from ViewData.ModelState instead of the value from ViewData.Model.
This happens at lines 366-367 of System.Web.Mvc.Html.InputExtensions:
string attemptedValue =
(string) htmlHelper.GetModelStateValue(fullName, typeof(string));
tagBuilder.MergeAttribute("value",
attemptedValue ?? ((useViewData)
? htmlHelper.EvalString(fullName)
: valueParameter), isExplicitValue);
As you can see, attemptedValue comes from ModelState. It contains the old value for CustomerDto.Name (the value that was posted to the controller action).
If this is a feature, why does it work this way? And is there a way to work around it? I would expect that if I update my model, the update gets rendered, not the old value I posted.
Well yes it's a feature (ModelState is always checked before actual Model), you can clear the ModelState, or update just the value you need:
ModelState["Name"].Value = updatedCustomer.Name;
In my view I have
<%:Html.LabelFor(model => model.IPAddress)%>
<div class="editor-field">
<%:Html.TextBoxFor(model => model.IPAddress)%>
<%:Html.ValidationMessageFor(model => model.IPAddress)%>
</div>
In my controller(post method), I have this
[HttpPost]
public ActionResult Manipulation(MyModel model){
//I change modele here
if(something)
model.IPAddress="100.100.100.100";
return View(model);
}
So, my question is:
When I change model, the TextBoxFor does not change his value.
TextBoxFor get his value when I get from get method to the post, and later I cannot change value of TextBoxFor.
I debug, and my model have new value, but TextBoxFor does not show new value.
Can you help me?
Try:
ModelState.Clear();
return View(model);
If not result! return a JSON Result and then update by javascript
Mr Grok had a similar problem this site. He had already found the ModelState.Clear() solution, but was wanting an explanation of why it worked. The highest ranked answer on the linked site proposed that the behavior of the html helper is a bug, for which ModelState.Clear() is a workaround. However, bradwils at this site says that the behavior is by design, and gives the following explanation:
The reason we use the posted value for editors rather than the model value is that the model may not be able to contain the value that the user typed. Imagine in your "int" editor the user had typed "dog". You want to display an error message which says "dog is not valid", and leave "dog" in the editor field. However, your model is an int: there's no way it can store "dog". So we keep the old value.
If you don't want the old values in the editor, clear out the Model State. That's where the old value is stored and pulled from the HTML helpers.
Despite the fact that it’s by design, this is very unexpected behavior for the developer, and it is unfortunate that interaction with the ModelState is required for a common programming need.
Furthermore, clearing the entire ModelState might cause unexpected issues in other areas (I think with respect to validation on unrelated model fields). So many thanks to Peter Gluck (responding in a comment within Mr Grok’s page) for proposing the more limited ModelState.Remove(“key”), and to Toby J for developing a more convenient method that works when you’re not sure what the key should be if the model property is nested. I also like Toby’s method because it doesn’t depend on a string as input.
That method, with minor changes, follows:
/// <summary>
/// Removes the ModelState entry corresponding to the specified property on the model. Call this when changing
/// Model values on the server after a postback, to prevent ModelState entries from taking precedence.
/// </summary>
/// <param name="model">The viewmodel that was passed in from a view, and which will be returned to a view</param>
/// <param name="propertyFetcher">A lambda expression that selects a property from the viewmodel in which to clear the ModelState information</param>
/// <remarks>
/// Code from Tobi J at https://stackoverflow.com/questions/1775170/asp-net-mvc-modelstate-clear
/// Also see comments by Peter Gluck, Metro Smurf and Proviste
/// Finally, see Bradwils http://forums.asp.net/p/1527149/3687407.aspx.
/// </remarks>
public static void RemoveStateFor<TModel, TProperty>(
this ModelStateDictionary modelState,
TModel model,
Expression<Func<TModel, TProperty>> propertyFetcher
) {
var key = ExpressionHelper.GetExpressionText(propertyFetcher);
modelState.Remove(key);
}
Instead of using model binding, id suggest using a tryupdate call.
[HttpPost]
public ActionResult Manipulation(FormCollection formCollection)
{
MyModel model = new MyModel();
if(TryUpdate(Model))
{
enter code here
}
if(somthing)
model.IPAddress="100.100.100.100";
return View(model);
}
Check out my answer to another post for the general structure i use. Its never failed me before and i believe it covers all bases when updating models from user input.
asp.net mvc controller post best practices
Here's another work around that I found.
Instead of
#Html.TextBoxFor(m=>m.PropertyName)
do this
#{
var myModel = Model;
}
#Html.TextBoxFor(m=>myModel.PropertyName)
This could be useful if you don't want to override the default behavior for every input.
Is it possible to disable a certain action parameter from retaining its value across requests?
[HttpPost]
public ActionResult MyAction(string value1, string value2)
{
if(value1=="hi")
ModelState.AddModelError("value1", "Can't have hi");
//do stuff
if(ModelState.IsValid)
return RedirectToAction("Finish");
else
return View()
}
[HttpGet]
public ActionResult MyAction()
{
return View()
}
The view consists of a simple form with two input boxes (value1 and value2). Once submitted and validation fails, the view is returned. I want to always have the value of the textbox in the view to be empty.
The value for the textbox "value1" is retained if the the model is invalidated.
I tried to declare the textbox as <%= Html.TextBox("value1", null) %> but the value is still retained. I also tried to use [Bind(Exclude="value1")] but that dosen't work on a single variable.
Update 2:
I'm doing this for a textbox that is used for Captcha (custom solution) input. I want the textbox to be cleared any time the page is loaded, but I want validation to remain.
Try calling
ModelState["value1"].Value
= new ValueProviderResult(null, string.Empty, CultureInfo.InvariantCulture);
before you return the view from within your controller action.
What this does is keep all the errors associated with the key "value1", but replaces the value with an empty value.
What are you doing that's causing it to be retained? There isn't anything like ViewState in MVC that will persist a value over multiple requests unless you're writing code or using form fields to make it do so.
What does the view look like? Is this action method being called via GET or POST? What's the "do stuff" contained in your method?
Edit: You're still showing //do stuff in your example code. Does that stuff contain any references to ViewData? Your question is about binding, but I don't see any binding happening. Maybe this is beyond my understanding.
Edit 2: Glad Phil saw this one! The original question didn't mention the ModelState.
I have a controller with two actions:
[AcceptVerbs("GET")]
public ActionResult Add()
{
PrepareViewDataForAddAction();
return View();
}
[AcceptVerbs("POST")]
public ActionResult Add([GigBinderAttribute]Gig gig, FormCollection formCollection)
{
if (ViewData.ModelState.IsValid)
{
GigManager.Save(gig);
return RedirectToAction("Index", gig.ID);
}
PrepareViewDataForAddAction();
return View(gig);
}
As you can see, when the form posts its data, the Add action uses a GigBinder (An implemenation of IModelBinder)
In this binder I have:
if (int.TryParse(bindingContext.HttpContext.Request.Form["StartDate.Hour"], out hour))
{
gig.StartDate.Hour = hour;
}
else
{
bindingContext.ModelState.AddModelError("Doors", "You need to tell us when the doors open");
}
The form contains a text box with id "StartDate.Hour".
As you can see above, the GigBinder tests to see that the user has typed in an integer into the textbox with id "StartDate.Hour". If not, a model error is added to the modelstate using AddModelError.
Since the gigs property gigs.StartDate.Hour is strongly typed, I cannot set its value to, for example, "TEST" if the user has typed this into the forms textbox.
Hence, I cant set the value of gigs.StartDate.Hour since the user has entered a string rather than an integer.
Since the Add Action returns the view and passes the model (return View(gig);) if the modelstate is invalid, when the form is re-displayed with validation mssages, the value "TEST" is not displayed in the textbox. Instead, it will be the default value of gig.StartDate.Hour.
How do I get round this problem? I really stuck!
I think the problem is that your ViewModel does not match closely enough with your View. It's really important in MVC that your ViewModel matches your View as closely as possible.
In your ViewModel you're assuming an integer, but in your View you're using a TextBox to render the property, which will allow any kind of text. There's a mismatch here and the difficulties you are experiencing trying to map them is a symptom of the mismatch.
I think you should either:
1. Change the type of the ViewModel property to string and then do validation in your controller to ensure the string entered is actually a number or:
2. Change the control that the View renders to a control that will only allow a number to be entered via a custom control or Javascript validation (as #Qun Wang recommends)
Personally, I'd recommend option 1. That way the ViewModel is not dependent on the View implementation.
Could you do this in your PrepareViewDataForAddAction method?..
if (!ViewData.ModelState.IsValid)
{
ViewData["StartDate.Hour"] = "Error";
}
The other fields on the form will still populate based on the properties of the Gig object.
I think you need to do some basic client side validation first.
don't allow it to post to the server.