Asp.Net MVC 3
I seem to have a similar problem as this post answered by Darin Dimitrov. So, Darin if you are reading this, please help :)
asp.net-mvc2 - Strongly typed helpers not using Model?
The problem I have is I am looking for an html helper that will contain the posted value in the modelstate.
For example, if I use an editor for like this:
#Html.EditorFor(model => model.SelectedTags)
I can see the value that was posted. The problem is I need a way to get this value without creating a textbox, I just want the string because I need it in some javascript.
I've tried DisplayFor, but that doesn't contain the posted value:
#Html.DisplayFor(model => model.SelectedTags)
By the way, I don't find this behavior intuitive AT ALL. I spent a few hours debugging ModelStateToTempDataAttribute from MVCContrib thinking it was a bug in their code to Import/Export Model State.
Thanks for any help!
Edit - Added Repro Code
Take these steps to reproduce:
Start project. Property1 should be blank (required), Property2 should have "abc"
Change Property2 to "xxx"
Submit Form (notice ClientValidationEnabled is False)
Form is posted, redirect, load (PRG). Property2 textbox has"xxx" and right below you will see "abc" from DisplayFor.
Controller
[ModelStateToTempData] //From MVCContrib
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
//simulate load from db
var model = new FormModel() { MyProperty2 = "abc" };
return View(model);
}
[HttpGet]
public ActionResult Success()
{
return View();
}
[HttpPost]
public ActionResult Index(FormModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Success");
}
else
{
return RedirectToAction("Index");
}
}
}
Model
public class FormModel
{
[Required]
public string MyProperty1 { get; set; }
public string MyProperty2 { get; set; }
}
View
#model MvcApplication4.Models.FormModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>FormModel</legend>
<div class="editor-label">
#Html.LabelFor(model => model.MyProperty1)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.MyProperty1)
#Html.ValidationMessageFor(model => model.MyProperty1)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.MyProperty2)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.MyProperty2)
#Html.ValidationMessageFor(model => model.MyProperty2)
</div>
#Html.DisplayFor(model => model.MyProperty2)
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
Config:
<add key="ClientValidationEnabled" value="false" />
ModelStateToTempData (MVCContrib):
public class ModelStateToTempDataAttribute : ActionFilterAttribute
{
public const string TempDataKey = "__MvcContrib_ValidationFailures__";
/// <summary>
/// When a RedirectToRouteResult is returned from an action, anything in the ViewData.ModelState dictionary will be copied into TempData.
/// When a ViewResultBase is returned from an action, any ModelState entries that were previously copied to TempData will be copied back to the ModelState dictionary.
/// </summary>
/// <param name="filterContext"></param>
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
var modelState = filterContext.Controller.ViewData.ModelState;
var controller = filterContext.Controller;
if(filterContext.Result is ViewResultBase)
{
//If there are failures in tempdata, copy them to the modelstate
CopyTempDataToModelState(controller.ViewData.ModelState, controller.TempData);
return;
}
//If we're redirecting and there are errors, put them in tempdata instead (so they can later be copied back to modelstate)
if((filterContext.Result is RedirectToRouteResult || filterContext.Result is RedirectResult) && !modelState.IsValid)
{
CopyModelStateToTempData(controller.ViewData.ModelState, controller.TempData);
}
}
private void CopyTempDataToModelState(ModelStateDictionary modelState, TempDataDictionary tempData)
{
if(!tempData.ContainsKey(TempDataKey)) return;
var fromTempData = tempData[TempDataKey] as ModelStateDictionary;
if(fromTempData == null) return;
foreach(var pair in fromTempData)
{
if (modelState.ContainsKey(pair.Key))
{
modelState[pair.Key].Value = pair.Value.Value;
foreach(var error in pair.Value.Errors)
{
modelState[pair.Key].Errors.Add(error);
}
}
else
{
modelState.Add(pair.Key, pair.Value);
}
}
}
private static void CopyModelStateToTempData(ModelStateDictionary modelState, TempDataDictionary tempData)
{
tempData[TempDataKey] = modelState;
}
}
you can read these values from modelstate dictionary like
<%:Html.ViewData.ModelState["key"] %>
However, it seems to me that SelectedTags is an enumeration of objects that is displayed for editing when you call EditorFor(model=>model.SelectedTags). in this scenario it is highly unlikely that you get anything by calling Html.ViewData.ModelState["SelectedTags"]. you will have to iterate over the keys in ModelState dictionary instead and check if key begins with SelectedTags prefix and then u can read its value accordinly.
In your Views -> Shared -> DisplayTemplates, create SelectedTags.cshtml
This will be your display template. Inside write something on the lines of
#model YourProject.WebUI.Models.SelectedTags
#for(int i = 0; i < Model.Tags.Count(); i++){
// Assuming that selected tags contains a list of tags.
// Replace <p> with whatever feels suitable
<p>Model.Tags[i]</p>
}
You can then use this display template in your views:
#Html.DisplayFor(model => model.SelectedTags,"SelectedTags")
This should also work:
#Html.DisplayFor(model => model.SelectedTags)
Related
I must have something incorrectly setup as I can't get the UpdateModel function to correctly update my model based on information passed in via a FormCollection.
My View looks like:
#model NSLM.Models.Person
#{
ViewBag.Title = "MVC Example";
}
<h2>My MVC Model</h2>
<fieldset>
<legend>Person</legend>
#using(#Html.BeginForm())
{
<p>ID: #Html.TextBox("ID", Model.ID)</p>
<p>Forename: #Html.TextBox("Forename", Model.Forename)</p>
<p>Surname: #Html.TextBox("Surname", Model.Surname)</p>
<input type="submit" value="Submit" />
}
</fieldset>
My model is:
namespace NSLM.Models
{
public class Person
{
public int ID;
public string Forename;
public string Surname;
}
}
and my controller is:
[HttpPost]
public ActionResult Details(FormCollection collection)
{
try
{
// TODO: Add update logic here
Models.Person m = new Models.Person();
// This doesn't work i.e. the model is not updated with the form values
TryUpdateModel(m);
// This does work
int.TryParse(Request.Form["ID"], out m.ID);
m.Forename = Request.Form["Forename"];
m.Surname = Request.Form["Surname"];
return View(m);
}
catch
{
return View();
}
}
as you can see if I manually assign each property it works fine, so what have I not set that would get the model to be updated with the form values?
Thanks,
Mark
Replace fields with properties in your model, i.e.:
namespace NSLM.Models
{
public class Person
{
public int ID {get; set;}
public string Forename {get; set;}
public string Surname {get; set;}
}
}
By the time the call gets to the action method any automatic model binding has already been performed. Try changing the input parameter of your action method to accept a Person instance. In that case the model binder will try to create the instance and populate it from the values passed by your form.
try this :
view :
#model NSLM.Models.Person
#{
ViewBag.Title = "MVC Example";
}
<h2>My MVC Model</h2>
<fieldset>
<legend>Person</legend>
#using(#Html.BeginForm())
{
#Html.HiddenFor(model => model.ID)
<p>Forename: #Html.EditorFor(model => model.Name)
#Html.ValidationMessageFor(model => model.Name)
</p>
<p>Surname: #Html.EditorFor(model => model.Surname)
#Html.ValidationMessageFor(model => model.Surname)
</p>
<input type="submit" value="Submit" />
}
</fieldset>
Controller :
[HttpPost]
public ActionResult Details(Person p)
{
if (ModelState.IsValid)
{
db.Entry(p).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(p);
}
I'm following [Getting started with ASP.NET MVC 3][1]. And I can't add/edit with value of Price = 9.99 or 9,99. It said: "The value '9.99' is not valid for Price." and "The field Price must be a number."
How to fix this?
Model:
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
public class MovieDbContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
}
Controller:
public class MovieController : Controller
{
private MovieDbContext db = new MovieDbContext();
//
// GET: /Movie/
public ViewResult Index()
{
var movie = from m in db.Movies
where m.ReleaseDate > new DateTime(1984, 6, 1)
select m;
return View(movie.ToList());
}
//
// GET: /Movie/Details/5
public ViewResult Details(int id)
{
Movie movie = db.Movies.Find(id);
return View(movie);
}
//
// GET: /Movie/Create
public ActionResult Create()
{
return View();
}
//
// POST: /Movie/Create
[HttpPost]
public ActionResult Create(Movie movie)
{
if (ModelState.IsValid)
{
db.Movies.Add(movie);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
//
// GET: /Movie/Edit/5
public ActionResult Edit(int id)
{
Movie movie = db.Movies.Find(id);
return View(movie);
}
//
// POST: /Movie/Edit/5
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
//
// GET: /Movie/Delete/5
public ActionResult Delete(int id)
{
Movie movie = db.Movies.Find(id);
return View(movie);
}
//
// POST: /Movie/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
Movie movie = db.Movies.Find(id);
db.Movies.Remove(movie);
db.SaveChanges();
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
}
}
View:
#model MvcMovies.Models.Movie
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"> </script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.ReleaseDate)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.ReleaseDate)
#Html.ValidationMessageFor(model => model.ReleaseDate)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Genre)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Genre)
#Html.ValidationMessageFor(model => model.Genre)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Price)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Price)
#Html.ValidationMessageFor(model => model.Price)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
public DbSet<Movie> Movies { get; set; }
}
I just stumbled on this again after 2 years. I thought ASP.NET MVC 5 had solved this but looks like it's not the case. So here goes how to solve the problem...
Create a class called DecimalModelBinder like the following and add it to the root of your project for example:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace YourNamespace
{
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if(valueResult.AttemptedValue != string.Empty)
{
try
{
actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
catch(FormatException e)
{
modelState.Errors.Add(e);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
Inside Global.asax.cs, make use of it in Application_Start() like this:
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());
You are one of the non-English customers, which MS has not foreseen. You will need to put some extra effort into making your version run. I had a similar problem, denying me both "9,99" and "9.99" as valid numbers. It seems like once server-side validation failed, and once client-side validation, causing no number to be accepted.
So you have to make the validation congruent.
Like suggested in the comments, have a look at
http://msdn.microsoft.com/en-us/library/gg674880(VS.98).aspx
and
http://haacked.com/archive/2010/05/10/globalizing-mvc-validation.aspx
and
MVC 3 jQuery Validation/globalizing of number/decimal field
or - should you understand German (or just look at the code examples)
http://www.andreas-reiff.de/2012/06/probleme-mit-mvcmovies-beispiel-validierung-des-preises-mit-dezimalstellen-schlagt-fehl/
BTW, same problem exists for both the Music and Movie example tutorials.
I encountered this issue when developing a web application for an English audience, on a Pc in The Netherlands.
A model property of type double, generated this server-side validation error:
The value '1.5' is not valid for .
On an breakpoint, I saw these values in the Immediate Window:
?System.Threading.Thread.CurrentThread.CurrentUICulture
{en-US}
?System.Threading.Thread.CurrentThread.CurrentCulture
{nl-NL}
As a solution (or maybe a work-around), you can specify the globalization settings in the web.config file.
<configuration>
<system.web>
<globalization culture="en" uiCulture="en" />
Of course this means that you force your users to enter numbers in English formatting, but that is just fine, in my case.
In 2019, this problem is still not solved. Using ASP Core 2.1, my UI is in French (decimal separator= ',') and I couldn't get the validation to work anytime I had a decimal number.
I found a workaround, not ideal though: I created a french-based CultureInfo but I changed the decimal separator to be the same as in Invariant Culture : '.'.
This made the trick, my decimal numbers are now displayed US style (but I am ok with it) and validation works.
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
//Culture specific problems
var cultureInfo = new CultureInfo("fr-FR");
cultureInfo.NumberFormat.NumberDecimalSeparator = ".";
System.Threading.Thread.CurrentThread.CurrentUICulture = cultureInfo;
}
I've adapted the code from Leniel Macaferi a little bit so you can use it for any type:
public class RequestModelBinder<TBinding> : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (valueResult.AttemptedValue != string.Empty)
{
try
{
// values really should be invariant
actualValue = Convert.ChangeType(valueResult.AttemptedValue, typeof(TBinding), CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
I've tried #Leniel Macaferi but it didn't work for me.
ModelState.IsValid didn't accept numbers formatted like 7.000,00
The problem started when I changed the property type from:
[Column("PRICE")]
public decimal Price { get; set; }
to
[Column("PRICE")]
public decimal? Price { get; set; }
I've also tried to include the globalization on web.config that I had forgotten
<globalization culture="pt-BR" uiCulture="pt-BR" enableClientBasedCulture="true" />
The only workaround that worked was change back the property to decimal only:
[Column("PRICE")]
public decimal Price { get; set; }
and also changed the table column to NOT accept null values
Hope it helps somebody.
You can add:
protected void Application_BeginRequest()
{
var currentCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone();
currentCulture.NumberFormat.NumberDecimalSeparator = ".";
currentCulture.NumberFormat.NumberGroupSeparator = " ";
currentCulture.NumberFormat.CurrencyDecimalSeparator = ".";
Thread.CurrentThread.CurrentCulture = currentCulture;
//Thread.CurrentThread.CurrentUICulture = currentCulture;
}
To Global.asax (tested on MVC 5.1). It works without changing UICulture for me.
I solved this problem by disabled jquery for price and only validate on server side for that input. I found the answer here:
ASP .NET MVC Disable Client Side Validation at Per-Field Level
<div class="col-md-10">
#{ Html.EnableClientValidation(false); }
#Html.EditorFor(model => model.DecimalValue, new { htmlAttributes = new { #class = "form-control" } })
#{ Html.EnableClientValidation(true); }
#Html.ValidationMessageFor(model => model.DecimalValue, "", new { #class = "text-danger" })
</div>
This is an extension of Leniel Maccaferri solution that avoids problems related to the sending by the user-agent of decimal values in a different culture format from the server one. Its limits are bound to the thousands separator parsing that, when it is the only separator in the value, can raise wrong bindings.
/// <summary>
/// custom decimal model binder
/// </summary>
/// <author>https://stackoverflow.com/users/114029/leniel-maccaferri</author>
/// <see cref="https://stackoverflow.com/a/19339424/3762855"/>
/// <remarks>Integrated with a fix for the decimal separator issue.
/// <para>This issue maybe depends from browsers interpretation of decimal values posted-back to the server when they receive response without any http content-language specific indication.</para>
/// <para>Important! decimal values caming from UI must not be formatted with thousands separator.</para>
/// </remarks>
public class DecimalModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
if (valueResult.AttemptedValue != string.Empty)
{
try
{
var culture = Thread.CurrentThread.CurrentCulture;
//This is needed to convert in the right manner decimal values coming from UI, as seems they always represent the decimal separator as a period(".").
//Maybe depends from browsers interpretation of decimal values posted-back to the server when they receive response without any http content-language specific indication.
if (culture.NumberFormat.NumberDecimalSeparator == "," && valueResult.AttemptedValue.LastIndexOf(".") > 0)
{
culture = new CultureInfo("en");
}
else if (Thread.CurrentThread.CurrentCulture.NumberFormat.NumberDecimalSeparator == "." && valueResult.AttemptedValue.LastIndexOf(",") > 0)
{
culture = new CultureInfo("it");
}
actualValue = Convert.ToDecimal(valueResult.AttemptedValue, culture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Just comment this link for the script:
<%--<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>--%>
I'm trying to learn the basics of MVC (NerdDinner tutorial). I have defined a model:
public class DinnerFormViewModel
{
// Properties
public Dinner Dinner { get; private set; }
public SelectList Countries { get; private set; }
// Constructor
public DinnerFormViewModel(Dinner dinner)
{
Dinner = dinner;
Countries = new SelectList(PhoneValidator.Countries, dinner.Country);
}
}
and I defined a partial view:
#model MyNerddiner.Models.DinnerFormViewModel
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Dinner</legend>
#Html.HiddenFor(model => model.Dinner.DinnerID)
<div class="editor-label">
#Html.LabelFor(model => model.Dinner.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Dinner.Title)
#Html.ValidationMessageFor(model => model.Dinner.Title)
</div>
</fieldset>
}
which is loaded from usual view:
#model MyNerddiner.Models.DinnerFormViewModel
#{
ViewBag.Title = "Create";
}
<div id="Create" >
<h2>Host a Dinner</h2>
#Html.Partial("_DinnerForm")
</div>
The controller:
public ActionResult Create()
{
Dinner dinner = new Dinner()
{
EventDate = DateTime.Now.AddDays(7)
};
return View(new DinnerFormViewModel(dinner));
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(DinnerFormViewModel dinnerViewModel)
{
Dinner dinner = null;
if (ModelState.IsValid)
{
try
{
dinner = dinnerViewModel.Dinner;
UpdateModel(dinner);
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id = dinner.DinnerID });
}
catch
{
ModelState.AddRuleViolations(dinner.GetRuleViolations());
return View(dinner);
}
}
return View(new DinnerFormViewModel(dinner));
}
Now when I'm trying to create (on postback), I'm getting an error:
No parameterless constructor defined for this object.
I can guess that it is because somewhere the program is trying to initiate the DinnerFormViewModel, but where, and why and how should I make it right?
The MVC framework needs your view model to have a constructor that takes no parameters so that it can create an empty instance to populate with data from the request. DinnerFormViewModel does not implement a constructor with no parameters, add one, and this will fix your issue.
Well, found the problem and it have nothing to do with model and constructor.
the problem was that view contained following row:
#Html.DropDownListFor(model => model.Countries, Model.Countries)
#Html.ValidationMessageFor(model => model.Countries)
When i checked from where the exception came- it come because the country value was null.
After i changed
model => model.Countries
to
model => model.Dinner.Country
the exception stoped to be thrown
I'm so glad i solve this on my own!
I am trying to get ASP.NET MVC 3 to generate forms from complex, nested objects. There is one validation behaviour I found which was unexpected and I am not sure if it's a bug in the DefaultModelBinder or not.
If I have two objects, lets call the "parent" one "OuterObject", and it has a property of type "InnerObject" (the child):
public class OuterObject : IValidatableObject
{
[Required]
public string OuterObjectName { get; set; }
public InnerObject FirstInnerObject { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
}
}
}
Here is InnerObject:
public class InnerObject : IValidatableObject
{
[Required]
public string InnerObjectName { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(InnerObjectName) && string.Equals(InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "InnerObjectName" });
}
}
}
You will notice the validation I put on both.. just some dummy validation to say some value can't equal "test".
Here is the view that this will display in (Index.cshtml):
#model MvcNestedObjectTest.Models.OuterObject
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm()) {
<div>
<fieldset>
<legend>Using "For" Lambda</legend>
<div class="editor-label">
#Html.LabelFor(m => m.OuterObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.OuterObjectName)
#Html.ValidationMessageFor(m => m.OuterObjectName)
</div>
<div class="editor-label">
#Html.LabelFor(m => m.FirstInnerObject.InnerObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.FirstInnerObject.InnerObjectName)
#Html.ValidationMessageFor(m => m.FirstInnerObject.InnerObjectName)
</div>
<p>
<input type="submit" value="Test Submit" />
</p>
</fieldset>
</div>
}
..and finally here is the HomeController:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new OuterObject();
model.FirstInnerObject = new InnerObject();
return View(model);
}
[HttpPost]
public ActionResult Index(OuterObject model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
return View(model);
}
}
What you will find is that when the model gets validated by the DefaultModelBinder, the "Validate" method in "InnerObject" gets hit twice, but the "Validate" method in "OuterObject" does not get hit at all.
If you take off IValidatableObject from "InnerObject", then the one on "OuterObject" will get hit.
Is this a bug, or should I expect it to work that way? If I should expect it to, what's the best workaround?
This answer is just to provide one workaround I have just thought of - so it is not really an answer! I am still not sure if this is a bug or what the best workaround is, but here is one option.
If you remove the custom validation logic from "InnerObject" and incorporate it into "OuterObject" it seems to work fine. So basically this works around the bug by only allowing the top-most object to have any custom validation.
Here is the new InnerObject:
//NOTE: have taken IValidatableObject off as this causes the issue - we must remember to validate it manually in the "Parent"!
public class InnerObject //: IValidatableObject
{
[Required]
public string InnerObjectName { get; set; }
}
And here is the new OuterObject (with the Validation code stolen from InnerObject):
public class OuterObject : IValidatableObject
{
[Required]
public string OuterObjectName { get; set; }
public InnerObject FirstInnerObject { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(OuterObjectName) && string.Equals(OuterObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("OuterObjectName must not be 'test'", new[] { "OuterObjectName" });
}
if (FirstInnerObject != null)
{
if (!string.IsNullOrWhiteSpace(FirstInnerObject.InnerObjectName) &&
string.Equals(FirstInnerObject.InnerObjectName, "test", StringComparison.CurrentCultureIgnoreCase))
{
yield return new ValidationResult("InnerObjectName must not be 'test'", new[] { "FirstInnerObject.InnerObjectName" });
}
}
}
}
This works as I would expect, hooking up the validation error to each field correctly.
It is not a great solution because if I need to nest "InnerObject" in some other class, it does not share that validation - I need to replicate it. Obviously I could have a method on the class to store the logic, but each "parent" class needs to remember to "Validate" the child class.
I am not sure this is a problem with MVC 4 anymore, but...
If you use partial views made just for your InnerObjects, they will validate correctly.
<fieldset>
<legend>Using "For" Lambda</legend>
<div class="editor-label">
#Html.LabelFor(m => m.OuterObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.OuterObjectName)
#Html.ValidationMessageFor(m => m.OuterObjectName)
</div>
#Html.Partial("_InnerObject", Model.InnerObject)
<p>
<input type="submit" value="Test Submit" />
</p>
</fieldset>
Then add this partial "_InnerObject.cshtml":
#model InnerObject
<div class="editor-label">
#Html.LabelFor(m => m.InnerObjectName)
</div>
<div class="editor-field">
#Html.TextBoxFor(m => m.InnerObjectName)
#Html.ValidationMessageFor(m => m.InnerObjectName)
</div>
Should you have made OuterObject base class for InnerObject instead of creating a relationship as you did? (Or vice versa) and provide the view the base object as the ViewModel?
This will mean that when model binding the default constructor of the OuterObject (or which ever class is your base) will be called indirectly invoking Validate on both objects.
i.e.
Class:
public class OuterObject : InnerObject, IValidateableObject
{
...
}
View:
#model MvcNestedObjectTest.Models.OuterObject
Controller Action:
public ActionResult Index(OuterObject model)
In my MVC program, once the user has submitted an edit form, those values for the object get saved in the model server-side, but the previous values show up in the view.
I know it has to do with MVC's validation process, where it first checks ModelState before the server side values. The solution I've read across the forums is to clear the ModelState. The only problem is, ModelState.Clear isn't working for me.
Help please.
Model
public class Help
{
[HiddenInput(DisplayValue=true)]
public int HelpID { get; set; }
[Required(ErrorMessage = "Please enter a proper URL")]
public string URL { get; set; }
[Required(ErrorMessage = "Please enter a content description:")]
[DataType(DataType.MultilineText)]
public string HelpContent { get; set; }
/*? 2 properites are nullable*/
public DateTime? createDateTime { get; set; }
public DateTime? modifiedDateTime { get; set; }
}
Controller
/*Create the admin controller*/
public class AdminController : Controller
{
//declare interface object
private IHelpRepository repository;
/*Pass a db interface to controller*/
public AdminController(IHelpRepository repo)
{
repository = repo;
}
/*default admin screen. displays help table obs*/
public ViewResult Index()
{
return View();
}
/*Returns add view form*/
public ActionResult AddForm()
{
return PartialView();
}
/*Will handle the post for the add screen after user has
submitted add information*/
[HttpPost]
[ValidateInput(false)] //this allows admin to place html in field
public ActionResult AddForm(Help help)
{
if (ModelState.IsValid) //if all fields are validated
{
//set the edit date
help.createDateTime = DateTime.Now;
repository.SaveHelp(help);
return (null); //return "null" to div so control is given back to main view
}
else //there is something wrong. send back to view
{
return PartialView(help);
}
}
/*Returns edit view form, searches for object to edit with id
if no id provided, 0 by default*/
public ActionResult EditForm(int helpID = 0)
{
Help help = repository.Help.FirstOrDefault(q => q.HelpID == helpID);
help.HelpContent = System.Web.HttpUtility.HtmlDecode(help.HelpContent);
return PartialView(help);
}
/*Will handle the post for the edit screen after user has
submitted edit information*/
[HttpPost]
[ValidateInput(false)] //this allows admin to place html in field
public ActionResult EditForm(Help help)
{
if (ModelState.IsValid) //if all fields are validated
{
//set the edit date
help.modifiedDateTime = DateTime.Now;
repository.SaveHelp(help);
ModelState.Clear();
return (null); //return "null" to div so control is given back to main view
}
else //there is something wrong. send back to view
{
return PartialView(help);
}
}
/*Delete action method, searches with id*/
[HttpPost]
public ActionResult Delete(int helpId)
{
Help helpDel = repository.Help.FirstOrDefault(p => p.HelpID == helpId);
if (helpDel != null) //if the object is found, delete
{
repository.DeleteHelp(helpDel);
}
//in all cases return to index
return RedirectToAction("Index");
}
/*Used by the telerik table to rebind grid*/
[GridAction]
public ActionResult AjaxBinding()
{
return View(new GridModel(repository.Help));
}
}//end admin controller class`
Partial View (Gets loaded into a div)
`
#using (Html.BeginForm(null, null, FormMethod.Post, new { id = "Editx" }))
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Edit Entry</legend>
#Html.HiddenFor(model => model.HelpID)
<div class="editor-label">
#Html.LabelFor(model => model.URL)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.URL)
#Html.ValidationMessageFor(model => model.URL)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.HelpContent, "Help Content")
</div>
<div class="editor-field">
#{
Html.Telerik().EditorFor(content => content.HelpContent)
.Name("HelpContent")
.FileBrowser(settings => settings
.Browse("Browse", "ImageBrowser")
.Thumbnail("Thumbnail", "ImageBrowser")
.Upload("Upload", "ImageBrowser")
.DeleteFile("DeleteFile", "ImageBrowser")
.DeleteDirectory("DeleteDirectory", "ImageBrowser")
.CreateDirectory("CreateDirectory", "ImageBrowser")
)
.Render();
}
#Html.ValidationMessageFor(model => model.HelpContent)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.createDateTime, "Create Date")
</div>
<div class="editor-field">
#Html.EditorFor(model => model.createDateTime)
#Html.ValidationMessageFor(model => model.createDateTime)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.modifiedDateTime, "Modified Date")
</div>
<div class="editor-field">
#Html.EditorFor(model => model.modifiedDateTime)
#Html.ValidationMessageFor(model => model.modifiedDateTime)
</div>
<p>
<input id="btnEdit" type="submit" value="Save" />
<button id="btnCancel">Cancel</button>
</p>
</fieldset>
}
After trolling through about a hundred links, I found the solution to my problem. ModelState.Clear actually clears the objects values in the controller, but for whatever reason was still displaying the old values in the view. Maybe its because I load/unload my edit form into a div tag? Don't ask. I don't know. The solution that works for me is this:
$.ajax({
url: "somecontroller/someAction,
cache: false, // this is key to make sure JQUERY does not cache your request
success: function( data ) {
alert( data ); } });
I had to set the "cache" setting to "false".
Thanks to #minus4 for the solution, bruh.