MVC 3 model value lost on 2nd post back - asp.net-mvc

I have a view with a model that has several fields. When I render the view on the GET I have a hidden field storing a code which is empty at this point. Then I POST and on the action I add a value to this code field through the model and send the model to the view like:
return View (model);
When the view renders, the hidden field does not have the code value but the view does contain all other values entered in the first step. So now when I post on a second button the model passed to the action does not contain the hidden code value I passed to it on the first post response.
If I updated the model on the first post sent it back to the view with new values, should I not get that code stored in a hidden input in the view available and being able to post it back again to the action?
I just also realized that if I change any model field on the first post back and send the updated model to the view it will only retain values from the first POST action. Do I have a cache issue here?, how do i manage this behavior? thanks

You should remove it from ModelState before changing the value in your POST action:
[HttpPost]
public ActionResult Foo(MyViewModel model)
{
// update the value of the model that was POSTed to some new value
model.SomeProperty = "some new value";
// remove POSTed value from the modelstate if you intend to modify it here
ModelState.Remove("SomeProperty");
return View(model);
}
The reason you need to do this is because Html helpers such as Html.HtextBoxFor, Html.HiddenFor, ... first use the value from modelstate when binding and then the value from your model. If you don't remove the value from ModelState then the HiddenFor helper will use the original POSTed value which is an empty string and not the value you modified in your action.

Issue is the same as Darin indicated. Here's an alternative solution:
public static MvcHtmlString MyHiddenFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes = null) {
var value = expression.Compile().Invoke(html.ViewData.Model);
TagBuilder input = new TagBuilder("input");
input.Attributes["type"] = "hidden";
input.Attributes["id"] = html.IdFor(expression).ToString();
input.Attributes["name"] = html.NameFor(expression).ToString();
if (htmlAttributes != null)
input.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
input.Attributes["value"] = value == null ? "" : value.ToString();
return new MvcHtmlString(input.ToString());
}

Related

How can I persist a check box list in MVC

I'm trying to build an html helper for creating a list of checkboxes, which will have the check state persisted using sessions. It works for the most part, remembering check box states when you check and uncheck various boxes and click submit. However, if you have boxes checked and submitted, and you go back and clear the checkboxes and resubmit (when they are ALL cleared) - it seems to want to remember the last selections. Here is what I've written...
[HomeController]
public ActionResult Index()
{
TestViewModel tvm = new TestViewModel();
return View(tvm);
}
[HttpPost]
public ActionResult Index(TestViewModel viewModel)
{
viewModel.SessionCommit();
return View(viewModel);
}
[Index View]
#model TestApp.Models.TestViewModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
<p>Checkboxes:</p>
#Html.CheckedListFor(x => x.SelectedItems, Model.CheckItems, Model.SelectedItems)
<input type="submit" name="Submit form" />
}
[TestViewModel]
// Simulate the checklist data source
public Dictionary<int, string> CheckItems
{
get
{
return new Dictionary<int, string>()
{
{1, "Item 1"},
{2, "Item 2"},
{3, "Item 3"},
{4, "Item 4"}
};
}
}
// Holds the checked list selections
public int[] SelectedItems { get; set; }
// Contructor
public TestViewModel()
{
SelectedItems = GetSessionIntArray("seld", new int[0] );
}
// Save selections to session
public void SessionCommit()
{
System.Web.HttpContext.Current.Session["seld"] = SelectedItems;
}
// Helper to get an int array from session
int[] GetSessionIntArray(string sessionVar, int[] defaultValue)
{
if (System.Web.HttpContext.Current.Session == null || System.Web.HttpContext.Current.Session[sessionVar] == null)
return defaultValue;
return (int[])System.Web.HttpContext.Current.Session[sessionVar];
}
[The HTML helper]
public static MvcHtmlString CheckedList(this HtmlHelper htmlHelper, string PropertyName, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
StringBuilder result = new StringBuilder();
foreach(var item in ListItems)
{
result.Append(#"<label>");
var builder = new TagBuilder("input");
builder.Attributes["type"] = "checkbox";
builder.Attributes["name"] = PropertyName;
builder.Attributes["id"] = PropertyName;
builder.Attributes["value"] = item.Key.ToString();
builder.Attributes["data-val"] = item.Key.ToString();
if (SelectedItemArray.Contains(item.Key))
builder.Attributes["checked"] = "checked";
result.Append(builder.ToString(TagRenderMode.SelfClosing));
result.AppendLine(string.Format(" {0}</label>", item.Value));
}
return MvcHtmlString.Create(result.ToString());
}
public static MvcHtmlString CheckedListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckedList(htmlHelper, name, ListItems, SelectedItemArray);
}
I've read this SO question and I think this may be to do with the model binder not knowing when there are no checkboxes checked, but even though I've gone through that and various other posts - I'm no further forward.
In one post, I saw that a hidden field is often used in combination with the checkbox to pass the 'false' state of the checkbox, but I couldn't get it working with multiple checkboxes posting back to a single property.
Can anyone shed light on this?
EDITED : to include the demonstration project I've highlighted in this post. Hopefully this will help someone to help me!
Your main issue, and the reason why the previous selections are being 'remembered' when you un-check all items is that you have a constructor in your model that calls GetSessionIntArray() which gets the values you stored last time you submitted the form. The DefaultModelBinder works by first initializing your model (including calling its default constructor) and then setting the values of its properties based the form values. In the following scenario
Step 1: Navigate to the Index() method
Assuming its the first call and no items have been added to Session, then the value of SelectedItems returned by GetSessionIntArray() is int[0], which does not match any values in CheckItems, so no checkboxes are checked.
Step 2: Check the first 2 checkboxes and submit.
The DefaultModelBinder initializes a new instance of TestViewModel and calls the constructor. The value of SelectedItems is again int[0] (nothing has been added to Session yet). The form values are then read and the value of SelectedItems is now int[1, 2] (the values of the checked checkboxes). The code inside the method is called and int[1, 2] is added to Session before returning the view.
Step 3: Un-check all checkboxes and submit again.
Your model is again initialized, but this time the constructor reads the values from Session and the value of SelectedItems is int[1,2]. The DefaultModelBinder reads the form values for SelectedItems, but there are none (un-checked checkboxes do not submit a value) so there is nothing to set and the value of SelectedItems remains int[1,2]. You then return the view and your helper checks the first 2 checkboxes based on the value of SelectedItems
You could solve this by removing the constructor from the model and modifying the code in the extension method to test for null
if (SelectedItemArray != null && SelectedItemArray.Contains(item.Key))
{
....
However there are other issues with you implementation, including
Your generating duplicate id attributes for each checkbox (your use of builder.Attributes["id"] = PropertyName;) which is invalid html.
builder.Attributes["data-val"] = item.Key.ToString(); makes no sense (it generates data-val="1", data-val="1" etc). Assuming you want attributes for unobtrusive client side validation, then the attributes would be data-val="true" data-val-required="The SelectedItems field is required.". But then you would need a associated placeholder for the error message (as generated by #Html.ValidationMessageFor() and the name attribute of each checkbox would need to be distinct (i.e. using indexers - name="[0].SelectedItems" etc).
Your using the value of the property for binding, but the correct approach (as all the built in extension method use) is to first get the value from ModelState, then from the ViewDataDictionary and finally if no values are found, then the actual model property.
You never use the value of var metadata = ModelMetadata..... although you should be (so that you can remove the last parameter (int[] SelectedItemArray) from the method, which is in effect just repeating the value of expression.
Side note: The use of a hidden field is not applicable in your case. The CheckboxFor() method generates the additional hidden input because the method binds to a bool property, and it ensures a value is always submitted.
My recommendation would be to use a package such as MvcCheckBoxList (I have not tried that one myself as I have my own extension method), at least until you spend some time studying the MVC source code to better understand how to create HtmlHelper extension methods (apologies if that sounds harsh).

passing value in partial view viewdatadictionary

#Html.Partial("~/Areas/WO/Views/PartialContent/_FirstPage.cshtml", new ViewDataDictionary { { "WOID", WOID } })
In my Page i am accessing Partial view in the above way.
I need to pass WOID(view data dictionary) value from query string, For that i am using following Code
#{
var desc = Html.ViewContext.HttpContext.Request.QueryString.Get("ID");
Uri referrer = HttpContext.Current.Request.UrlReferrer;
string[] query = referrer.Query.Split('=');
int WOID = Convert.ToInt32(query[1]);
}
But the issue is this code is working in all browsers except I.E. i Need to Solve this problem.
Please help me
Instead of this you can have this value as part of you model and use that.That is the standard and recommeded way .
In your action method you can have these as parameter.Your query string value will get bind to this parameter
public ActionResult ActionMethod(int ID)
{
Model.WOID = WOID;
// Other logic
return View(Model)
}
Next step you can add this as a property to your view model or add it to ViewData dictionary and then access it in your partial view.

How can I return a view(model) and also pass a query string param to the view?

I have an MVC app where users fill in a 4-step form then go to a "confirm" screen. On the confirm screen, if they select to modify their info I use RedirectToAction to take them back to the first step view, and I pass a URL parameter "modify=true", which tells the controller to use the session object already created as opposed to creating a new object from the DB and displaying an empty form. But once they submit the form for step 1 I want to send them from my controller to the step 2 view along with the "modify=true" parameter. There doesn't seem to be a way to return a viewmodel to a view and also pass a query string parameter. How can I accomplish this?
I have considered adding a bool to the viewmodels to signify "inReview" but i use different viewmodels for each of these views and they're all pretty clean, it seems like this bool would muck things up a bit.
I have also considered adding the bool to viewbag or viewdata, but then i'd be using the submit button to pass that value and the "modify=true" parameter would drop off the URL, possibly confusing the user and definitely confusing the code.
Thanks
If you use the Html.BeginForm() helper (without parameters) it will automatically append existing query string parameters to the generated form action attribute. If you use some of the other overloads such as Html.BeginForm("SomeAction", "SomeController", FormMethod.Post) then you're gonna lose those parameters. This could be easily fixed by writing a custom helper that will take into account those parameters:
public static class HtmlHelpers
{
public static IDisposable BeginRequestForm(this HtmlHelper html, string action, string controller, FormMethod method)
{
var builder = new TagBuilder("form");
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var routeValues = new RouteValueDictionary();
var query = html.ViewContext.HttpContext.Request.QueryString;
foreach (string key in query)
{
routeValues[key] = query[key];
}
builder.MergeAttribute("action", urlHelper.Action(action, controller, routeValues));
builder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true);
html.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag));
return new MvcForm(html.ViewContext);
}
}
and then use in your view (after bringing it into scope of course):
#using (Html.BeginRequestForm("SomeAction", "SomeController", FormMethod.Post))
{
...
}
You can either use ViewBag or your view model. You just need to pass the value somehow to the view:
ViewBag.modify = true;
return View(model);
Then in your view:
Html.BeginForm("MyAction", "MyController", new { modify = ViewBag.modify })

Why does creating a new ViewModel return the same data as the old ViewModel?

I'm just learning MVC3 now and this is really confusing me.
I have a ViewModel that contains some child ViewModels. Each of the ChildViewModels get rendered with a different Partial View, and when submitting execute a different action on the Controller. All the ChildViewModels should perform some custom validation on their data, and if successful it should move on to the next page. If the validation fails, it should simply return to the ParentView and display the errors.
[HandleError]
public class MyController: Controller
{
public ActionResult Index()
{
var viewModel = new ParentViewModel();
return View("ParentView", viewModel);
}
[HttpPost]
public ActionResult ChildViewModelB_Action(ChildViewModelB viewModel)
{
if (ModelState.IsValid)
{
return View("ChildViewModelB_Page2", viewModel);
}
else
{
// I'm having trouble returning to the ParentView and
// simply displaying the ChildViewModel's errors, however
// discovered that creating a new copy of the VM and displaying
// the ParentView again shows the existing data and any errors
// But why??
var vm = new ParentViewModel();
return View("ParentView", vm);
}
}
}
For example,
The page loads with 3 options.
User selects option B and fills out a form.
Upon submit, the child ViewModel B gets validated and fails.
Page returns to ParentView, with ChildB all filled out, however ChildB errors are now also showing.
Why does creating a new copy of the ParentViewModel display the ParentView with the same data as the original ParentViewModel?
And is there a different way I should be returning to the ParentView after doing server-side validation?
You need to clear the modelstate if you intend to modify values in your POST action
else
{
ModelState.Clear();
var vm = new ParentViewModel();
return View("ParentView", vm);
}
The reason for that is because Html helper such as TextBoxFor will first look in the modelstate when binding their values and after that in the model. And since the modelstate already contains the POSTed values, that's what's used => the model is ignored. This is by design.
This being said the correct thing to do in your case is to simply redirect to the GET action which already blanks the model and respect the Redirect-After-Post pattern:
else
{
return RedirectToAction("Index");
}
Why does creating a new copy of the ParentViewModel display the
ParentView with the same data as the original ParentViewModel?
Because the values of the fields are retrieved from the POSTed form and not from the model. That makes sense right? We don't want the user to show a form filled with different values from what they submitted.

How to modify posted form data within controller action before sending to view?

I want to render the same view after a successful action (rather than use RedirectToAction), but I need to modify the model data that is rendered to that view. The following is a contrived example that demonstrates two methods that that do not work:
[AcceptVerbs("POST")]
public ActionResult EditProduct(int id, [Bind(Include="UnitPrice, ProductName")]Product product) {
NORTHWNDEntities entities = new NORTHWNDEntities();
if (ModelState.IsValid) {
var dbProduct = entities.ProductSet.First(p => p.ProductID == id);
dbProduct.ProductName = product.ProductName;
dbProduct.UnitPrice = product.UnitPrice;
entities.SaveChanges();
}
/* Neither of these work */
product.ProductName = "This has no effect";
ViewData["ProductName"] = "This has no effect either";
return View(product);
}
Does anyone know what the correct method is for accomplishing this?
After researching this further, I have an explanation why the following code has no effect in the Action:
product.ProductName = "This has no effect";
ViewData["ProductName"] = "This has no effect either";
My View uses HTML Helpers:
<% Html.EditorFor(x => x.ProductName);
HTML Helpers uses the following order precedence when attempting lookup of the key:
ViewData.ModelState dictionary entry
Model property (if a strongly typed view. This property is a shortcut to View.ViewData.Model)
ViewData dictionary entry
For HTTP Post Actions, ModelState is always populated, so modifying the Model (product.ProductName) or ViewData directly (ViewData["ProductName"]) has no effect.
If you do need to modify ModelState directly, the syntax to do so is:
ModelState.SetModelValue("ProductName", new ValueProviderResult("Your new value", "", CultureInfo.InvariantCulture));
Or, to clear the ModelState value:
ModelState.SetModelValue("ProductName", null);
You can create an extension method to simplify the syntax:
public static class ModelStateDictionaryExtensions {
public static void SetModelValue(this ModelStateDictionary modelState, string key, object rawValue) {
modelState.SetModelValue(key, new ValueProviderResult(rawValue, String.Empty, CultureInfo.InvariantCulture));
}
}
Then you can simply write:
ModelState.SetModelValue("ProductName", "Your new value");
For more details, see Consumption of Data in MVC2 Views.
The values are stored in ModelState.
This should do what you want:
ModelState.SetModelValue("ProductName", "The new value");
I wouldn't suggest doing that though... the correct method would be to follow the PRG (Post/Redirect/Get) pattern.
HTHs,
Charles
EDIT: Updated to reflect the better was of setting the ModelState value as found by #Gary
This will trigger the model to re-evaluate under simple conditions:
ModelState.Clear();
model.Property = "new value";
TryValidateModel(model);
Perform ModelState.Clear() before you change the model.
...
ModelState.Clear()
dbProduct.ProductName = product.ProductName;
dbProduct.UnitPrice = product.UnitPrice;
...

Resources