I'm trying to figure out why this simple controller action isn't working. All I'm trying to do is increase Number after every POST.
Model
public class ViewModel
{
public int Number { get; set; }
}
View
<body>
<% using (Html.BeginForm("Test", "Invoice"))
{ %>
<%: Html.EditorFor(m => m.Number) %>
<%= Model.Number %>
<input type="submit" value="Submit" />
<% } %>
</body>
Controller
public ActionResult Test()
{
var viewModel = new ViewModel {Number = 1};
return View("Test", viewModel);
}
[HttpPost]
public ActionResult Test(ViewModel viewModel)
{
viewModel.Number = viewModel.Number + 1;
return View("Test", viewModel);
}
In my controller, viewModel.Number is increased to 2, but when the view is returned the text box contains 1 and Model.Number displays 2.
Am I missing something?
The Html helpers favor the values in the ModelState over the actual Model values.
So if you modify the Model in your action you need to clear the ModelSate before passing it to the view:
[HttpPost]
public ActionResult Test(ViewModel viewModel)
{
viewModel.Number = viewModel.Number + 1;
ModelState.Clear();
return View("Test", viewModel);
}
You can read more about this ASP.NET MVC feature in this great article:
ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes
Related
I am using ASP.Net MVC with C#. I have a model which has a member for filter criteria. This member is a IList>. The key contains value to display and the value tells if this filter is selected or not. I want to bind this to bunch of checkboxes on my view. This is how I did it.
<% for(int i=0;i<Model.customers.filterCriteria.Count;i++) { %>
<%=Html.CheckBoxFor(Model.customers.filterCriteria[i].value)%>
<%=Model.customers.filterCriteria[i].key%>
<% } %>
This displays all checkboxes properly. But when I submit my form, in controller, I get null for filtercriteria no matter what I select on view.
From this post I got a hint for creating separate property. But how will this work for IList..? Any suggestions please?
The problem with the KeyValuePair<TKey, TValue> structure is that it has private setters meaning that the default model binder cannot set their values in the POST action. It has a special constructor that need to be used allowing to pass the key and the value but of course the default model binder has no knowledge of this constructor and it uses the default one, so unless you write a custom model binder for this type you won't be able to use it.
I would recommend you using a custom type instead of KeyValuePair<TKey, TValue>.
So as always we start with a view model:
public class Item
{
public string Name { get; set; }
public bool Value { get; set; }
}
public class MyViewModel
{
public IList<Item> FilterCriteria { get; set; }
}
then a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
FilterCriteria = new[]
{
new Item { Name = "Criteria 1", Value = true },
new Item { Name = "Criteria 2", Value = false },
new Item { Name = "Criteria 3", Value = true },
}
});
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// The model will be correctly bound here
return View(model);
}
}
and the corresponding ~/Views/Home/Index.aspx view:
<% using (Html.BeginForm()) { %>
<%= Html.EditorFor(x => x.FilterCriteria) %>
<input type="submit" value="OK" />
<% } %>
and finally we write a customized editor template for the Item type in ~/Views/Shared/EditorTemplates/Item.ascx or ~/Views/Home/EditorTemplates/Item.ascx (if this template is only specific to the Home controller and not reused):
<%# Control
Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<AppName.Models.Item>"
%>
<%= Html.CheckBoxFor(x => x.Value) %>
<%= Html.HiddenFor(x => x.Name) %>
<%= Html.Encode(Model.Name) %>
We have achieved two things: cleaned up the views from ugly for loops and made the model binder successfully bind the checkbox values in the POST action.
Maybe I'm missing something but when I have a form that posts back to the same action, the textbox value reverts to the old value. The following example should increment the value in the textbox on each POST. This does not happen, the value on the model is incremented and the model is valid.
IF however I clear the modelstate in the HttpPost Action (the comment in the code), everything works as expected.
Am I missing something?
Here's the code:
Model:
public class MyModel
{
public int MyData { get; set; }
}
View:
<%# Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.MyModel>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm()) {%>
<%: Html.TextBoxFor(m => m.MyData)%> (<%: Model.MyData%>)
<%: Html.ValidationMessageFor(m => m.MyData) %> <br />
State :<%: ViewData["State"] %> <br />
<input type="submit" />
<% } %>
</asp:Content>
Controller:
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View(new MyModel { MyData = 0 });
}
[HttpPost]
public ActionResult Index(MyModel myModel)
{
// ModelState.Clear();
ViewData["State"] = "invalid";
if (ModelState.IsValid)
ViewData["State"] = "Valid";
var model = new MyModel { MyData = myModel.MyData + 1 };
return View(model);
}
}
I just found an answer to this online.
The trick is to clear the ModelState before returning the Model
[HttpPost]
public ActionResult Index(MyModel myModel)
{
// ModelState.Clear();
ViewData["State"] = "invalid";
if (ModelState.IsValid)
ViewData["State"] = "Valid";
var model = new MyModel { MyData = myModel.MyData + 1 };
ModelState.Clear();
return View(model);
}
For more detail read these 2 articles
http://forums.asp.net/p/1527149/3687407.aspx
Asp.net MVC ModelState.Clear
You should either use the Post-Redirect-Get pattern or not use the Html Helpers.
Reference: http://blogs.msdn.com/b/simonince/archive/2010/05/05/asp-net-mvc-s-html-helpers-render-the-wrong-value.aspx
Basically MVC expects any redisplay from a post to be a validation error, and re-uses the posted data (view ModelState) for redisplay in preference to model data. The guidance is to not use ModelState.Clear().
I have a ViewModel with a property as below:
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString="{0:d}", ApplyFormatInEditMode=true)]
public DateTime ConfigDate { get; set; }
The Form that displays the ConfigDate is as below:
<%= Html.EditorFor(Model => Model.ConfigDate)%>
When the Index Action comes back, everything looks formatted correctly, i.e. the <input> box has the date value as 12/12/2001. When the form is posted, the result that comes back is as though the DisplayFormat attribute isn't being applied.
EDIT:
More info was requested: here is the code en toto:
The Search Form
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Config.Web.Models.AirplanesViewModel>" %>
<% using (Html.BeginForm("Details", "Airplanes", FormMethod.Post, new { id = "SearchForm" })) { %>
<%= Html.LabelFor(model => model.ConfigDate) %>
<%= Html.EditorFor(Model => Model.ConfigDate)%>
<input id="searchButton" type="submit" value="Search" />
<% } %>
The AirplanesViewModel
public class AirplanesViewModel
{
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString="{0:d}", ApplyFormatInEditMode=true)]
public DateTime ConfigDate { get; set; }
}
}
The Controller
[HttpGet]
public ActionResult Index()
{
AirplanesViewModel avm = new AirplanesViewModel
{
ConfigDate = DateTime.Now
};
return View(avm);
}
[HttpPost]
[ActionName("Details")]
public ActionResult Details_Post(AirplanesViewModel avm)
{
return RedirectToAction("Details", avm);
}
[HttpGet]
public ActionResult Details(AirplanesViewModel avm)
{
int page = 0;
int pageSize = 10;
if (!ModelState.IsValid)
{
avm.Airplanes = new PaginatedList<Airplane>();
return View(avm);
}
try
{
Query q = new Query(avm.Query);
PaginatedList<Airplane> paginatedPlanes = new PaginatedList<Airplane>(repo.ByQuery(q), page, pageSize);
avm.Airplanes = paginatedPlanes;
return View(avm);
}
catch (Exception)
{
// Should log exception
avm.Airplanes = new PaginatedList<Airplane>();
return View(avm);
}
}
Additional Information
It has something to do with the redirection to the GET Action. When I take out the POST Action and remove the GET attribute (so both GET and POST use the Details() Action) the problem goes away - but this also gets rid of my pretty URL's when the form is submitted (and causes the annoying "are you sure?" popup on refresh). Strangely, the only problem is the loss of formatting in that field. Everything else is fine.
While waiting for you to clearly specify the problem, here's a full working counter example that what you describe in your question doesn't actually happen:
Model:
public class MyViewModel
{
[DisplayName("As Configured On:")]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime ConfigDate { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
ConfigDate = DateTime.Now
};
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
View:
<% using (Html.BeginForm()) { %>
<%= Html.EditorFor(x => x.ConfigDate) %>
<input type="submit" value="OK" />
<% } %>
You can submit the form as much as you wish, the formatting will be preserved.
UPDATE:
After providing additional information here's why the problem occurs. When you redirect to the Details action with return RedirectToAction("Details", avm); a query string parameter is applied to the url:
http://localhost:1114/Airplanes/Details?ConfigDate=11/30/2010%2000:00:00
Notice how the hour is included and that's normal. Now when you return the view in the Details GET action the HTML helper responsible for generating the editor template will do the following tasks:
Check to see whether there's a ConfigDate parameter (either GET or POST). If none was found check the value of the Model which is passed to the view and use the ConfigValue property of the model and generate the textbox.
As a ConfigValue is found in the query string the model is not used at all. So it simply takes the value passed in the redirect which also contains the time and uses it to bind to it.
Model:
public class Model
{
public ItemType Type { get; set; }
public int Value { get; set; }
}
public enum ItemType { Type1, Type2 }
Controller:
public ActionResult Edit()
{
return View(new Model());
}
[HttpPost]
public ActionResult Edit(Model model, bool typeChanged = false)
{
if (typeChanged)
{
model.Value = 0; // I need to update model here and pass for further editing
return View(model);
}
return RedirectToAction("Index");
}
And of course View:
<div class="editor-label"><%: Html.LabelFor(model => model.Type) %></div>
<div class="editor-field">
<%: Html.DropDownListFor(
model => model.Type,
Enum.GetNames(typeof(MvcApplication1.Models.ItemType))
.Select(x => new SelectListItem { Text = x, Value = x }),
new { #onchange = "$(\'#typeChanged\').val(true); this.form.submit();" }
)
%>
<%: Html.Hidden("typeChanged") %>
</div>
<div class="editor-label"><%: Html.LabelFor(model => model.Value) %></div>
<div class="editor-field"><%: Html.TextBoxFor(model => model.Value) %></div>
<input type="submit" value="Create" onclick="$('#typeChanged').val(false); this.form.submit();" />
The code in controller (with the comment) doesn't work as I expect. How could I achieve the needed behavior?
As I wrote here multiple times, that's how HTML helpers work and it is by design: when generating the input they will first look at the POSTed value and only after that use the value from the model. This basically means that changes made to the model in the controller action will be completely ignored.
A possible workaround is to remove the value from the modelstate:
if (typeChanged)
{
ModelState.Remove("Value");
model.Value = 0; // I need to update model here and pass for futher editing
return View(model);
}
I have a section of a view that I would like to submit to an action so it is setup inside a Html.BeginForm()
I am trying to also make use of the Html.Telerik().DatePicker() control and would like it also to strongly type linked inside the form, same as the DropDownListFor, is this possible?
<% using (Html.BeginForm("MyAction", "Responding")) { %>
<%= Html.DropDownListFor(m => m.SelectedItem, Model.MyItems) %>
<!--list works fine-->
<% Html.Telerik().DatePicker()
.Name("datePicker")
.Render(); %>
<!--how do I get this strongly-type-linked?-->
<input type="submit" value="submit" />
<% } %>
The model for the view has the appropriate properties:
public int SelectedItem { get; set; }
public string SelectedDate { get; set; }
Controller code:
public class RespondingController : Controller
{
[HttpGet]
public ActionResult MyAction(int SelectedItem, string SelectedDate)
{
//on the form submit SelectedItem value arrives
//need SelectedDate to do the same
}
}
Your parameter is incorrect. it needs to be named the same as the name value you gave to the control e.g 'datePicker'.
public class RespondingController : Controller
{
[HttpGet]
public ActionResult MyAction(int SelectedItem, DateTime? datePicker)
{
//on the form submit SelectedItem value arrives
//need SelectedDate to do the same
}
}