MVC 1 Parameter Binding - c#

I'm passing a date to my server in Invariant Culture, the following format
'mm/dd/yy'
The parameter binding in MVC fails to parse this date and returns null for the parameter. This is persumably because IIS is running on a machine using English culture ('dd/mm/yy' works just fine).
I want to override the parsing of all date on my server to use Invariant Culture like so...
Convert.ChangeType('12/31/11', typeof(DateTime), CultureInfo.InvariantCulture);
even when the date is part of another object...
public class MyObj
{
public DateTime Date { get; set; }
}
My controller method is something like this....
public ActionResult DoSomethingImportant(MyObj obj)
{
// use the really important date here
DoSomethingWithTheDate(obj.Date);
}
The date is being sent as Json data like so....
myobj.Date = '12/31/11'
I've tried adding an implementation of IModelBinder to the binderDictionary in the global.asax
binderDictionary.Add(typeof(DateTime), new DateTimeModelBinder());
That doesn't work, and neither does
ModelBinders.Binders.Add(typeof(DateTime), new DataTimeModelBinder());
This seems like some ppl would want to do all the time. I can't see why you would parse dates etc. in the current culture on the server. A client would have to find out the culture of the server just to format dates the server will be able to parse.....
Any help appreciated!

I've solved the problem here, what I had missed was that in the object, the datetime was nullable
public class MyObj
{
public DateTime? Date { get; set; }
}
Hence my binder wasn't being picked up.
If anyone is interested, this is what I did....
In the global.asax added the following
binderDictionary.add(typeof(DateTime?), new InvariantBinder<DateTime>());
Created an invariant binder like so
public class InvariantBinder<T> : IModelBinder
{
public object BindModel(ControllerContext context, ModelBindingContext binding)
{
string name = binding.ModelName;
IDictionary<string, ValueProviderResult> values = binding.ValueProvider;
if (!values.ContainsKey(name) || string.IsNullOrEmpty(values[names].AttemptedValue)
return null;
return (T)Convert.ChangeType(values[name].AttemptedValue, typeof(T), CultureInfo.Invariant);
}
}
Hope this comes in handy for someone else.....

Is your problem that your custom model binder is unable to parse some of the input dates or that your custom model binder never gets called? If it's the former then trying to use the culture of the user's browser might help.
public class UserCultureDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
object value = controllerContext.HttpContext.Request[bindingContext.ModelName];
if (value == null)
return null;
// Request.UserLanguages could have multiple values or even no value.
string culture = controllerContext.HttpContext.Request.UserLanguages.FirstOrDefault();
return Convert.ChangeType(value, typeof(DateTime), CultureInfo.GetCultureInfo(culture));
}
}
...
ModelBinders.Binders.Add(typeof(DateTime?), new UserCultureDateTimeModelBinder());

Is it possible to pass the date to the server in ISO 8601 format? I think the server would parse that correctly regardless of its own regional settings.

Related

Json.NET deserializing DateTimeOffset value fails for DateTimeOffset.MinValue without timezone

In my ASP.NET Core Web-API project, I'm getting a HTTP POST call to one of my API controllers.
While evaluating the JSON payload and deserializing its contents, Json.NET stumbles upon a DateTime value of 0001-01-01T00:00:00 and can't convert it to a DateTimeOffset property.
I notice that the value propably should represent the value of DateTimeOffset.MinValue, but its lack of a timezone seems to trip the deserializer up. I can only imagine that the DateTimeOffset.Parse tries to translate it to the hosts current timezone, which results in an underflow of DateTimeOffset.MinValue.
The property is pretty simplistic:
[JsonProperty("revisedDate", NullValueHandling = NullValueHandling.Ignore)]
public DateTimeOffset? RevisedDate { get; set; }
And here is the response sent to the client:
{
"resource.revisedDate": [
"Could not convert string to DateTimeOffset: 0001-01-01T00:00:00. Path 'resource.revisedDate', line 20, position 44."
]
}
I'm using Newtonsoft.Json v11.0.2 and currently am in UTC + 2 (Germany). The exception traceback and error message are here: https://pastebin.com/gX9R9wq0.
I can't fix the calling code, so I have to fix it on my side of the line.
But the question is: How?
The problem seems reproducible only when the machine's time zone TimeZoneInfo.Local has a positive offset from UTC, e.g. (UTC+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna. I was unable to reproduce it in time zones with a non-positive offset such as UTC-05:00 or UTC itself.
Specifically, in JsonReader.ReadDateTimeOffsetString() a call is made to DateTimeOffset.TryParse using DateTimeStyles.RoundtripKind:
if (DateTimeOffset.TryParse(s, Culture, DateTimeStyles.RoundtripKind, out dt))
{
SetToken(JsonToken.Date, dt, false);
return dt;
}
This apparently causes an underflow error in time zones with a positive UTC offset. If in the debugger I parse using DateTimeStyles.AssumeUniversal instead, the problem is avoided.
You might want to report an issue about this to Newtonsoft. The fact that deserialization of a specific DateTimeOffset string fails only when the computer's time zone has certain values seems wrong.
The workaround is to use IsoDateTimeConverter to deserialize your DateTimeOffset properties with IsoDateTimeConverter.DateTimeStyles set to DateTimeStyles.AssumeUniversal. In addition it is necessary to disable the automatic DateTime recognition built into JsonReader by setting JsonReader.DateParseHandling = DateParseHandling.None, which must be done before the reader begins to parse the value for your DateTimeOffset properties.
First, define the following JsonConverter:
public class FixedIsoDateTimeOffsetConverter : IsoDateTimeConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?);
}
public FixedIsoDateTimeOffsetConverter() : base()
{
this.DateTimeStyles = DateTimeStyles.AssumeUniversal;
}
}
Now, if you can modify the JsonSerializerSettings for your controller, use the following settings:
var settings = new JsonSerializerSettings
{
DateParseHandling = DateParseHandling.None,
Converters = { new FixedIsoDateTimeOffsetConverter() },
};
If you cannot easily modify your controller's JsonSerializerSettings you will need to grab DateParseHandlingConverter from this answer to How to prevent a single object property from being converted to a DateTime when it is a string and apply it as well as FixedIsoDateTimeOffsetConverter to your model as follows:
[JsonConverter(typeof(DateParseHandlingConverter), DateParseHandling.None)]
public class RootObject
{
[JsonProperty("revisedDate", NullValueHandling = NullValueHandling.Ignore)]
[JsonConverter(typeof(FixedIsoDateTimeOffsetConverter))]
public DateTimeOffset? RevisedDate { get; set; }
}
DateParseHandlingConverter must be applied to the model itself rather than the RevisedDate property because the JsonReader will already have recognized 0001-01-01T00:00:00 as a DateTime before the call to FixedIsoDateTimeOffsetConverter.ReadJson() is made.
Update
In comments, #RenéSchindhelm writes, I created an issue to let Newtonsoft know. It is Deserialization of DateTimeOffset value fails depending on system's timezone #1731.
This is what I am using to fix the issue in .NET Core 3.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.Ignore;
options.SerializerSettings.DateParseHandling = DateParseHandling.None;
options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal });
});
...
Change DateTimeOffset to DateTime solved the problem.
Check your Json.NET version and then your input value and formatting. I'm trying the following example and it is working fine for me:
void Main()
{
var json = #"{""offset"":""0001-01-01T00:00:00""}";
var ds = Newtonsoft.Json.JsonConvert.DeserializeObject<TestDS>(json);
Console.WriteLine(ds);
}
public class TestDS {
[Newtonsoft.Json.JsonProperty("offset", NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public DateTimeOffset? DSOffset { get; set; }
}
Here is the output:
DSOffset 1/1/0001 12:00:00 AM -06:00

ASP.NET MVC How to handle dynamic datetime format from database

In my ASP.NET MVC 5 dwith EF 6 project, I have a database where datetime format is stored as string like "dd-MM-yyyy". User can change this format any time. User will use the given format in the date fields in the view. But when they will post that. Automatically it will bind as a DateTime for that property. I am statically handling it by the following code
[DataType(DataType.Time), DisplayFormat(DataFormatString = "{HH:mm}", ApplyFormatInEditMode = true)]
public DateTime? EndingTime { get; set; }
public string EndingTimeValue
{
get
{
return EndingTime.HasValue ? EndingTime.Value.ToString("HH:mm") : string.Empty;
}
set
{
EndingTime = DateTime.Parse(value);
}
}
but I know it's not a best way to do that. There may need a model binder or filter or any kind of custom attribute. I will be greatly helped if you give me a efficient solution with sample code. Thanks in advance.
NB: I am using razor view engine. and my solution consists of 7 projects. So there is no chance of using Session in model. Again I have a base repository class for using entity framework.
People usually store the datetime in the database as a datetime.
Then wherever you do a translation from datetime to string that datetime can be displayed in a format that depends on the culture of the viewer.
By doing this you can quickly make a page with datetime formats that will format the datetimes nicely wherever you are.
change the culture you pass to the toString and the format changes.
please see this MSDN page for more info about it.
edit: (see comments below)
anywhere on server:
string WhatYouWant = yourTime.ToCustomFormat()
and create an extension method for the datetime that gets the format out of the database and returns a string in the correct format.
public static class MyExtensions
{
public static string ToCustomFormat(this DateTime yourTime)
{
// Get the following var out of the database
String format = "MM/dd/yyyy hh:mm:sszzz";
// Converts the local DateTime to a string
// using the custom format string and display.
String result = yourTime.ToString(format);
return result;
}
}
This will allow you to call it anywhere anytime on your server. You can't access the method client side in javascript. I hope this helps.
(To be honest I'm a new developer too and still have a lot to learn ^^)
I have tried many options regarding this problem. Now what I am doing is created an action filter to catch all the DateTime and nullable DateTime Fields. Here I am providing the binder.
public class DateTimeBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
DateTime date;
var displayFormat = SmartSession.DateTimeFormat;
if (DateTime.TryParseExact(value.AttemptedValue, displayFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out date))
{
return date;
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName,"Invalid Format");
}
return base.BindModel(controllerContext, bindingContext);
}
}
in views the code I am formatting the date using same date format.

How do I disable model validation on a single field in C# MVC?

I have a view model that looks vaguely like:
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
I have a custom ModelBinder to parse the fields in the form, and assign the values to them. Which works.
However, when an error occurs, my ModelState ends up with two errors in it. The first from my ModelBinder, and the second from (I guess) the default validation rules:
- Invalid start date selected <-- My custom error message.
- The value 'fgfdg' is not valid for Start Date. <-- I want this to go away
How do I turn the default validation off for a specific field, on the server side?
Edit: Before you ask, yes my ModelBinder is extending DefaultModelBinder, but obviously I need the other default model binding behaviour; it's just these fields I want a custom behavior for.
(Why don't I just use standard validation rules? Because this is a search form, and depending on if the 'custom date range' is selected, we either ignore the StartDate and EndDate, or parse and perform various checks on them. Specifically, it is an absolute requirement that if the date ranges are invalid (eg. 'fdafsfsf' for start date, but 'search by XXX' instead of 'search by date range' is selected, the form must submit successfully without error)
Code fragment:
[ModelBinderType(typeof(MyViewModel))]
public class MyViewModelBinder : DefaultModelBinder {
public override object BindModel(ControllerContext cc, ModelBindingContext bc) {
var model = new MyViewModel();
var searchType = cc.HttpContext.Request["SearchType"];
if (searchType == "CustomDateRange") {
// Do checks here, etc.
// ONLY if searchType == "CustomDateRange" should there be ANY validation on StartDate
bc.ModelState.AddModelError("StartDate", "Invalid start date; outside of invoice range");
}
// bc.ModelState["StartDate"].Errors.Clear(); <--- Clears my error, not the default one.
bc.ModelMetadata.Model = model;
return base.BindModel(cc, bc);
}
}
You should be able to call ModelState.Errors.Clear on the specific property. E.g:
if (someCondition) {
bindingContext.ModelState["StartDate"].Errors.Clear();
return base.BindModel(......
}
Clear the ModelState then call the DefaultModelBinder implementation..

mvc validate date/time is at least 1 minute in the future

I am still getting my hands around MVC.
I have seen several similar questions, some custom code and various methods but I have not found something that works for me.
I have a search model that fills an HTML table with results inside of a partial view. I have this in my search results model:
public DateTime? BeginDateTime { get; set; }
Which is set to DateTime.Now in the controller. The user can specify that date and time to run a task with the search results' data on the model's POST call.
What I would like to do is validate that the date/time the user defined is at least 1 minute in the future. If this can be done as a client-side validation it will be better, but I am open to options as long as it works.
View:
Begin update: #Html.TextBoxFor(o => o.BeginDateTime, new { id="txtBegin" })
Thanks.
Create a new Attribute:
public class FutureDateAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
return value != null && (DateTime)value > DateTime.Now;
}
}
Now in your model set this attribute:
[FutureDate(ErrorMessage="Date should be in the future.")]
public DateTime Deadline { get; set; }
This is another good way to check that the date selected is from the future.
public class FutureDate : ValidationAttribute
{
public override bool IsValid(object value)
{
DateTime dateTime;
var isValid = DateTime.TryParseExact(
//Getting the value from the user.
Convert.ToString(value),
//We want the user to enter date in this format.
"d mmm yyyy",
//It checks if the culture is us-en
CultureInfo.CurrentCulture,
//Mosh has no idea what this does.
DateTimeStyles.None,
//Output parameter.
out dateTime);
return (isValid && dateTime > DateTime.Now);
}
}

Binding error in custom model minder removes the value the user inputted

I'm using ASP.NET MVC 3 RTM, and I have a view model like this:
public class TaskModel
{
// Lot's of normal properties like int, string, datetime etc.
public TimeOfDay TimeOfDay { get; set; }
}
The TimeOfDay property is a custom struct I have, which is quite simple, so I'm not including it here. I've made a custom model binder to bind this struct. The model binder is pretty simple:
public class TimeOfDayModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
try
{
// Let the TimeOfDay struct take care of the conversion from string.
return new TimeOfDay(result.AttemptedValue, result.Culture);
}
catch (ArgumentException)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Value is invalid. Examples of valid values: 6:30, 16:00");
return bindingContext.Model; // Also tried: return null, return value.AttemptedValue
}
}
}
My custom model binder works fine, but the problem is when the user provided value could not be converted or parsed. When this happens (When the TimeOfDay constructor throws an ArgumentException), I add a model error which is correctly displayed in the view, but the value the user typed, which could not be converted, is lost. The textbox the user typed the value in, is just empty, and in the HTML source the value attribute is set to an empty string: "".
EDIT: I'm wondering if it might be my editor template which is doing something wrong, so I'm including it here:
#model Nullable<TimeOfDay>
#if (Model.HasValue)
{
#Html.TextBox(string.Empty, Model.Value.ToString());
}
else
{
#Html.TextBox(string.Empty);
}
How do I make sure the value is not lost when a binding error happens, so the user can correct the value?
Aha! I finally found the answer! This blog post gave the answer. What I was missing is to call ModelState.SetModelValue() in my model binder. So the code would be like this:
public class TimeOfDayModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
try
{
// Let the TimeOfDay struct take care of the conversion from string.
return new TimeOfDay(result.AttemptedValue, result.Culture);
}
catch (ArgumentException)
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Value is invalid. Examples of valid values: 6:30, 16:00");
// This line is what makes the difference:
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result);
return bindingContext.Model;
}
}
}
I hope this saves someone else from the hours of frustration I've been through.

Categories