ASP.NET MVC datetime culture issue when passing value back to controller - asp.net-mvc-3

How can i tell my controller/model what kind of culture it should expect for parsing a datetime?
I was using some of this post to implement jquery datepicker into my mvc application.
When i submit the date it gets "lost in translation" i'm not using the US formatting for the date, so when it gets sent to my controller it simply becomes null.
I have a form where the user chooses a date:
#using (Html.BeginForm("List", "Meter", FormMethod.Get))
{
#Html.LabelFor(m => m.StartDate, "From:")
<div>#Html.EditorFor(m => m.StartDate)</div>
#Html.LabelFor(m => m.EndDate, "To:")
<div>#Html.EditorFor(m => m.EndDate)</div>
}
I've made an edit template for this, to implement the jquery datepicker:
#model DateTime
#Html.TextBox("", Model.ToString("dd-MM-yyyy"), new { #class = "date" })
I then create the datepicker widgets like this.
$(document).ready(function () {
$('.date').datepicker({ dateFormat: "dd-mm-yy" });
});
All this works fine.
Here is where the problems start, this is my controller:
[HttpGet]
public ActionResult List(DateTime? startDate = null, DateTime? endDate = null)
{
//This is where startDate and endDate becomes null if the dates dont have the expected formatting.
}
This is why i would like to somehow tell my controller what culture it should expect?
Is my model wrong? can i somehow tell it which culture to use, like with the data annotation attributes?
public class MeterViewModel {
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
}
Edit: this link explains my issue and a very good solution to it aswell. Thanks to gdoron

you can change the default model binder to use the user culture using IModelBinder
public class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var date = value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
return date;
}
}
And in the Global.Asax write:
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeBinder());
Read more at this excellent blog that describe why Mvc framework team implemented a default Culture to all users.

You can create a Binder extension to handle the date in the culture format.
This is a sample I wrote to handle the same problem with Decimal type, hope you get the idea
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;
try
{
actualValue = Convert.ToDecimal(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
Update
To use it simply declare the binder in Global.asax like this
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
//HERE you tell the framework how to handle decimal values
ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
DependencyResolver.SetResolver(new ETAutofacDependencyResolver());
}
Then when the modelbinder has to do some work, it will know automatically what to do.
For example, this is an action with a model containing some properties of type decimal. I simply do nothing
[HttpPost]
public ActionResult Edit(int id, MyViewModel viewModel)
{
if (ModelState.IsValid)
{
try
{
var model = new MyDomainModelEntity();
model.DecimalValue = viewModel.DecimalValue;
repository.Save(model);
return RedirectToAction("Index");
}
catch (RulesException ex)
{
ex.CopyTo(ModelState);
}
catch
{
ModelState.AddModelError("", "My generic error message");
}
}
return View(model);
}

This issue arises because you are using the GET method on your Form. The QueryString Value Provider in MVC always uses Invariant/US date format. See: MVC DateTime binding with incorrect date format
There are three solutions:
Change your method to POST.
As someone else says, change the date format to ISO 8601 "yyyy-mm-dd" before submission.
Use a custom binder to always treat Query String dates as GB. If you do this you have to make sure that all dates are in that form:
public class UKDateTimeModelBinder : IModelBinder
{
private static readonly ILog logger = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
/// <summary>
/// Fixes date parsing issue when using GET method. Modified from the answer given here:
/// https://stackoverflow.com/questions/528545/mvc-datetime-binding-with-incorrect-date-format
/// </summary>
/// <param name="controllerContext">The controller context.</param>
/// <param name="bindingContext">The binding context.</param>
/// <returns>
/// The converted bound value or null if the raw value is null or empty or cannot be parsed.
/// </returns>
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var vpr = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (vpr == null)
{
return null;
}
var date = vpr.AttemptedValue;
if (String.IsNullOrEmpty(date))
{
return null;
}
logger.DebugFormat("Parsing bound date '{0}' as UK format.", date);
// Set the ModelState to the first attempted value before we have converted the date. This is to ensure that the ModelState has
// a value. When we have converted it, we will override it with a full universal date.
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, bindingContext.ValueProvider.GetValue(bindingContext.ModelName));
try
{
var realDate = DateTime.Parse(date, System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB"));
// Now set the ModelState value to a full value so that it can always be parsed using InvarianCulture, which is the
// default for QueryStringValueProvider.
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, new ValueProviderResult(date, realDate.ToString("yyyy-MM-dd hh:mm:ss"), System.Globalization.CultureInfo.GetCultureInfoByIetfLanguageTag("en-GB")));
return realDate;
}
catch (Exception)
{
logger.ErrorFormat("Error parsing bound date '{0}' as UK format.", date);
bindingContext.ModelState.AddModelError(bindingContext.ModelName, String.Format("\"{0}\" is invalid.", bindingContext.ModelName));
return null;
}
}
}

When submitting a date you should always try and submit it in the format "yyyy-MM-dd". This will allow for it to become culture independent.
I normally have a hidden field which maintains the date in this format. This is relatively simple using jQuery UI's datepicker.

Why not simply inspect the culture of the data and convert it as such? This simple approach allowed me to use strongly typed dates in models, show action links and edit fields in the desired locale and not have to fuss at all binding it back into a strongly typed DateTime:
public class DateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
return value.ConvertTo(typeof(DateTime), value.Culture);
}
}

that did the trick for me
<system.web>
<globalization enableClientBasedCulture="true" uiCulture="Auto" culture="Auto" />
</system.web>

I have a updated solution for MVC5 based on the Post of #gdoron. I will share it in case anyone else is looking for this. The class inherits from DefaultModelBinder and has exception handling for invalid dates. It also can handle null values:
public class DateTimeModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object result = null;
var modelName = bindingContext.ModelName;
var attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;
// in datetime? binding attemptedValue can be Null
if (attemptedValue != null && !string.IsNullOrWhiteSpace(attemptedValue))
{
try
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
result = DateTime.Parse(value.AttemptedValue, CultureInfo.CurrentCulture);
}
catch (FormatException e)
{
bindingContext.ModelState.AddModelError(modelName, e);
}
}
return result;
}
}
And just like the mentioned sample in the Global.Asax write
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new DateTimeBinder());

Related

Validating a view model after custom model binding

I have a view model that implements IValidatableObject that contains a string and a collection of another view model, something like this:
public sealed class MainViewModel
{
public string Name { get; set; }
public ICollection<OtherViewModel> Others { get; set; }
}
My validation checks each object in Others against different rules using the contract provided by IValidatableObject:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var other in this.Others)
{
// validate or yield return new ValidationResult
}
}
Because of the complex structure of the real MainViewModel I have had to create a custom model binder which re-builds the model and assigns POST data to the relevant components. The problem that I'm getting is that nothing is getting validated resulting in validation errors at the context level as it violates certain database constraints and I'm not sure what I'm doing wrong - I assumed that ModelState.IsValid would invoke the Validate method on my view model but it doesn't seem to go down that way.
My model binder looks like this:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
int modelId = (int)controllerContext.RouteData.Values["id"];
// query the database and re-build the components of the view model
// iterate the POST data and assign to the model where necessary
// should I be calling something here to validate the model before it's passed to the controller?
return model;
}
Any help appreciated!
Validator.TryValidateObject
OK, seems I'm a little closer. I can now get my IValidatableObject method to run by adding the following to my custom model binder:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
Seems that Validator.TryValidateObject invokes the validation method and setting the last parameter to true causes it to validate all properties. However, I'm now stuck with getting the validationResults to the controller so they can be used in a meaningful way.
I should have realised that I could use the ModelState.AddModelError through a custom binder, I've managed to get this working correctly now by adding the following to my custom model binder before returning the model to the controller:
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model, new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
foreach (var result in validationResults)
{
bindingContext.ModelState.AddModelError("", result.ErrorMessage);
}
}
return model;
This now returns a list of all errors to my page and the ModelState.IsValid check on my controller action is now returning false.
Paul's great answer can be refactored into a generic validate-and-convert to ModelState method as follows (e.g. in a helper or CustomModelBinder base). In addition, the bindings to the validated properties are retained.
public static void DoValidation(ModelBindingContext bindingContext,
IValidatableObject model)
{
var validationResults = new HashSet<ValidationResult>();
var isValid = Validator.TryValidateObject(model,
new ValidationContext(model, null, null), validationResults, true);
if (!isValid)
{
var resultsGroupedByMembers = validationResults
.SelectMany(_ => _.MemberNames.Select(
x => new {MemberName = x ?? "",
Error = _.ErrorMessage}))
.GroupBy(_ => _.MemberName);
foreach (var member in resultsGroupedByMembers)
{
bindingContext.ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(_ => _.Error)));
}
}
}

Don't understand the mechanics of writing own validation attribute

I have written an attribute before, but I I have not written a validation attribute before. I am seriously confused about how it all works together. I have read most of the tutorials online about how to go about accomplishing this. But I am left with a couple of questions to ponder.
Keep in mind that I am trying to write a requiredIf attribute that will only call a remote function if a certain Jquery variable is set... which incidentally is a variable that is pulled from view state... I guess I could make that part of my view model. But I digress
1) The C# code is slightly confusing. I know my attribute should extend the ValidationAttribute, IClientValidatable class and interface respectively. But I am a little confused about what each of the overidden methods should be doing? I am trying to write a requiredIf, how does overwriting these methods help me accomplish this goal?
2) If the variable is not there, I simply don't want the remote function to attempt to validate the field. I don't want any message to pop up on my form. Alot of the tutorials seem to revolve around that.
3) I am confused about what I need to do with the jquery to add this function to the view... What do I need to add to the JQuery to get this thing to work... It seems like a lot of extra coding when I could simply just type up a jquery function that did the same thing with just the same ore less coding... I know it also adds server side validation which is good. But still...
Here is what I have for my jquery side of this equation...
(function ($) {
$validator.unobtrusive.adapters.addSingleVal("requiredifattribute", "Dependent");
$validator.addMethod("requiredifattribute", function (value, element, params) {
if (!this.optional(element)) {
var otherProp = $('#' + params)
return (otherProp.val() != value);
}
return true;
})
}(jQuery));
Here is my Attribute (which is basically carbon copied out of one the required if tutorials... I know I need to customize it more, but once I get a better idea of what every piece is doing I will do that...
[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfAttribute : ValidationAttribute, IClientValidatable {
private const string errorMessage = "The {0} is required.";
//public string
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue){
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var field = validationContext.ObjectInstance.GetType().GetProperty(DependentProperty);
if (field != null) {
var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
if ((dependentValue == null && TargetValue == null) || (dependentValue.Equals(TargetValue))) {
if (!innerAttribute.IsValid(value))
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
ModelClientValidationRule modelClientValidationRule = new ModelClientValidationRule {
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "requiredifattribute"
};
modelClientValidationRule.ValidationParameters.Add("dependent", DependentProperty);
yield return modelClientValidationRule;
}
}
UPDATE: What I have simply isn't working
Here is how a property in my model is anotated with the above attribute
[RequiredIf("isFlagSet", true)]
[Remote("ValidateHosFin", "EditEncounter", AdditionalFields = "hospitalFin, encflag", ErrorMessage = "Got Damn this is complex!")]
[MinLength(6)]
public string HostpitalFinNumber { get; set; }
The value in my view that I was trying to key this validation on is set up like so...
ViewData["ADDENCOREDITTEMP"] = encflag;
if (encflag == "AddEnc"){
isFlagSet = true;
}
I embed it into my page like so...
#Html.Hidden("isFlagSet", isFlagSet, new { id = "isFlagSet"})
I can't get my form to submit... The person who said he just tried this and got it to work, could you post the code?
Model:
public class X
{
[RequiredIf("y", "y", ErrorMessage = "y is not y")]
public string x { get; set; }
public string y { get; set; }
}
View:
#using(Html.BeginForm())
{
#Html.ValidationSummary()
#Html.TextBoxFor(m => m.x)
#Html.TextBoxFor(m => m.y)
<input type="submit"/>
}
I assume your validation fails on the server side? do you have isFlagSet property in your view model?

Error on TryUpdateModel

I'm getting an error with TryUpdateModel when the field doesn't have a value and it's not nullable in the database. If I change the field in the database to nullable, it works. Anybody know a fix to get this to work with non nullable fields? I'm new to MVC, so I'm just playing with a tutorial to try to understand it....
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(FormCollection form)
{
var movieToAdd = new Movies();
if (Request != null && Request.Form != null && Request.Form.HasKeys() && ValueProvider == null)
{
ValueProvider = new FormCollection(Request.Form).ToValueProvider();
}
//Deserialize (Include white list!)
TryUpdateModel(movieToAdd, new string[] { "Title", "Director" }, form.ToValueProvider());
// Validate
if (string.IsNullOrEmpty(movieToAdd.Title))
ModelState.AddModelError("Title", "Title is required!");
if (string.IsNullOrEmpty(movieToAdd.Director))
ModelState.AddModelError("Director", "Director is required!");
// If valid, save movie to database
if (ModelState.IsValid)
{
_db.AddToMovies1(movieToAdd);
_db.SaveChanges();
return RedirectToAction("Index");
}
// Otherwise, reshow form
return View(movieToAdd);
}
There are a few things I would change.
Modify your method signature to accept the strongly typed model that your ActionResult is expecting. From there you can check if the Model is valid or not and continue with saving to the database
Use strongly typed models and data annotations for validating your model
EG:
Create a model which you would use for the form Get And Post actions:
public class YourModel
{
[Required]
public string Title {get; set;]
[Required]
public string Director {get; set;}
}
Your Post ActionResult then becomes:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(YourModel postedform)
{
// If valid, save movie to database
if (ModelState.IsValid)
{
_db.AddToMovies1(postedform);
_db.SaveChanges();
return RedirectToAction("Index");
}
// Otherwise, reshow form
return View(postedform);
}

Using abstract view model in MVC 3

I have an input form containing several input fields. Each input field has a ElementModel which has properties basically for the label and the value. The input fields to display are specified in a XML document, so I have kind of a dynamic view with only a list of elements.
The problem is, that each element should be either displayed as a decimal or as percentage value. And of course, if it's a percentage value, the user shoud be able to input something like "45%" and the value in the model should then be 0.45.
My first thought when I found this article was to use an abstract view model class with an abstract property for the value and to define a PercentageElementModel deriving from my base ElementModelclass that makes use of a custom model binder. Unfortunately, if I use that abstract base class in my view, the data annotations made in the PercentageElementModelare ignored.
Do you have any idea of how I can solve this? I don't want to use strings in my view model and do the parsing by myself as this will break the MVC pattern. Are there some other ways to achieve my goal?
Here are some code snippets:
public abstract class ElementModel
{
public string ElementName { get; set; }
public ElementType ElementType { get; set; }
public abstract double? ElementValue { get; set; }
}
public class PercentageElementModel : ElementModel
{
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:P2}")]
public override double? ElementValue { get; set; }
}
I came up with another solution as my problem is more a matter of display formatting than of validation: I wrote a custom ModelBinder which checks the input string of the text box. If it ends with a trailing '%' sign, I will devide the value by 100. Here's the code.
public class DoubleModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
double? actualValue = null;
try
{
// cut trailing '%'
if (valueResult.AttemptedValue.EndsWith("%"))
{
var strValue = valueResult.AttemptedValue.Substring(0, valueResult.AttemptedValue.Length - 1);
actualValue = double.Parse(strValue, valueResult.Culture) / 100;
}
else
{
actualValue = Convert.ToDouble(valueResult.AttemptedValue, CultureInfo.CurrentCulture);
}
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
The rest happens in the view itself. To display the correct format, I use a simple ToString() method with a percentage format string assigned if needed. And if the user enters a numeric value without '%', a jQuery's blur() event will append the '%' sign to the user's input. Perfectly works for me although this is not the best answer for my own question.

Can I reuse a remote validation action in MVC3

I am using a Remote validation attribute on my view model to validate a Bank Account that is specified for my Company:
ViewModel:
[Remote("CheckDefaultBank", "Company")]
public string DefaultBank
{
This in the controller I have:
[HttpGet]
public JsonResult CheckDefaultBank(string defaultBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
That all works well. But, I have two other banks related to my company as well. However, when the remote validation js calls the action it uses a parameter mactching the field name of "DefaultBank"... so I use that as a parameter in my action.
Is there some attribute I can add in the view so that it will use a parameter of say "bankId" on the ajax get so I don't need an action for each field which are basically exactly the same?
The goal here is to eliminate now having to have this in my controller:
[HttpGet]
public JsonResult CheckRefundBank(string refundBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
[HttpGet]
public JsonResult CheckPayrollBank(string payrollBank)
{
bool result = BankExists(defaultBank);
return Json(result, JsonRequestBehavior.AllowGet);
}
I was hoping I could do something like this in the view:
#Html.EditorFor(model => model.DefaultBank, new { data-validate-parameter: bankId })
This way I could just use the same action for all of the Bank entries like:
[HttpGet]
public JsonResult CheckValidBank(string bankId)
{
bool result = BankExists(bankId);
return Json(result, JsonRequestBehavior.AllowGet);
}
Possible?
For just such a situation, I wrote a RemoteReusableAttribute, which may be helpful to you. Here is a link to it: Custom remote Validation in MVC 3
Since MVC uses the default model binder for this, just like a normal action method. You could take a FormsCollection as your parameter and lookup the value. However, I personally would find it much easier to just use several parameters to the function, unless you start having dozens of different parameters.
You could also write a custom model binder, that would translate the passed parameter to a generic one.
Consider encapsulating the logic, "BankExists" in this case into a ValidationAttribute (Data Annotations Validator). This allows other scenarios as well.
Then use a wrapper ActionResult like the one below, which lets you pass in any validator.
[HttpGet]
public ActionResult CheckRefundBank(string refundBank)
{
var validation = BankExistsAttribute();
return new RemoteValidationResult(validation, defaultBank);
}
Here is the code for the ActionResult that works generically with Validators.
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
public class RemoteValidationResult : ActionResult
{
public RemoteValidationResult(ValidationAttribute validation, object value)
{
this.Validation = validation;
this.Value = value;
}
public ValidationAttribute Validation { get; set; }
public object Value { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var json = new JsonResult();
json.JsonRequestBehavior = JsonRequestBehavior.AllowGet;
if (Validation.IsValid(Value))
{
json.Data = true;
}
else
{
json.Data = Validation.FormatErrorMessage(Value.ToString());
}
json.ExecuteResult(context);
}
}
As an extra enhancement consider creating a Controller Extension method to dry up your return call even more.

Resources