Reputation: 105009
I would like to write my own model binder for DateTime
type. First of all I'd like to write a new attribute that I can attach to my model property like:
[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}
This is the easy part. But the binder part is a bit more difficult. I would like to add a new model binder for type DateTime
. I can either
IModelBinder
interface and write my own BindModel()
methodDefaultModelBinder
and override BindModel()
methodMy model has a property as seen above (Birth
). So when the model tries to bind request data to this property, my model binder's BindModel(controllerContext, bindingContext)
gets invoked. Everything ok, but. How do I get property attributes from controller/bindingContext, to parse my date correctly? How can I get to the PropertyDesciptor
of property Birth
?
Because of separation of concerns my model class is defined in an assembly that doesn't (and shouldn't) reference System.Web.MVC assembly. Setting custom binding (similar to Scott Hanselman's example) attributes is a no-go here.
Upvotes: 25
Views: 40214
Reputation: 3888
For ASP.NET Core, you can use the following custom model binder. A sample model is given below.
public class MyModel
{
public string UserName { get; set; }
[BindProperty(BinderType = typeof(CustomDateTimeBinder))]
public DateTime Date1 { get; set; }
[BindProperty(BinderType = typeof(CustomDateTimeBinder))]
public DateTime? Date2 { get; set; }
}
The custom binder for DateTime value. It expects the format dd/MM/yyyy
.
public class CustomDateTimeBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
if (!DateTime.TryParseExact(value, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateTime))
{
var fieldName = string.Join(" ", Regex.Split(modelName, @"(?<!^)(?=[A-Z])"));
bindingContext.ModelState.TryAddModelError(
modelName, $"{fieldName} is invalid.");
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(dateTime);
return Task.CompletedTask;
}
}
Upvotes: 1
Reputation: 150253
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);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
}
public class NullableDateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, value);
return value == null
? null
: value.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
}
And in the Global.Asax add the following to Application_Start():
ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new NullableDateTimeBinder());
Read more at this excellent blog that describe why Mvc framework team implemented a default Culture to all users.
Upvotes: 89
Reputation: 1
You could implement a custom DateTime Binder like so, but you have to take care about the assumed culture and value from the actual client request. May you get an Date like mm/dd/yyyy in en-US and want it to convert in the systems culture en-GB (which it would be like dd/mm/yyyy) or an invariant culture, like we do, then you have to parse it before and using the static facade Convert to change it in its behaviour.
public class DateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
var modelState = new ModelState {Value = valueResult};
var resDateTime = new DateTime();
if (valueResult == null) return null;
if ((bindingContext.ModelType == typeof(DateTime)||
bindingContext.ModelType == typeof(DateTime?)))
{
if (bindingContext.ModelName != "Version")
{
try
{
resDateTime =
Convert.ToDateTime(
DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture,
DateTimeStyles.AdjustToUniversal).ToUniversalTime(), CultureInfo.InvariantCulture);
}
catch (Exception e)
{
modelState.Errors.Add(EnterpriseLibraryHelper.HandleDataLayerException(e));
}
}
else
{
resDateTime =
Convert.ToDateTime(
DateTime.Parse(valueResult.AttemptedValue, valueResult.Culture), CultureInfo.InvariantCulture);
}
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return resDateTime;
}
}
Anyway, culture dependend DateTime parsing in a stateless Application can by a cruelty...Especially when you work with JSON on javascript clientside and backwards.
Upvotes: 0
Reputation: 2485
I had this very big problem myself and after hours of try and fail I got a working solution like you asked.
First of all since having a binder on just a property is not possibile yuo have to implement a full ModelBinder. Since you don't want the bind all the single property but only the one you care you can inherit from DefaultModelBinder and then bind the single property:
public class DateFiexedCultureModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType == typeof(DateTime?))
{
try
{
var model = bindingContext.Model;
PropertyInfo property = model.GetType().GetProperty(propertyDescriptor.Name);
var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
if (value != null)
{
System.Globalization.CultureInfo cultureinfo = new System.Globalization.CultureInfo("it-CH");
var date = DateTime.Parse(value.AttemptedValue, cultureinfo);
property.SetValue(model, date, null);
}
}
catch
{
//If something wrong, validation should take care
}
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
In my example I'm parsing date with a fiexed culture, but what you want to do is possible. You should create a CustomAttribute (like DateTimeFormatAttribute) and put it over you property:
[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}
Now in the BindProperty method, instead of looking for a DateTime property you can look for a property with you DateTimeFormatAttribute, grab the format you specified in the constructor and then parse the date with DateTime.ParseExact
I hope this helps, it took me very long to come with this solution. It was actually easy to have this solution once I knew how to search it :(
Upvotes: 14
Reputation: 126547
I don't think you should put locale-specific attributes on a model.
Two other possible solutions to this problem are:
To answer your actual question, the way to get custom attributes (for MVC 2) is to write an AssociatedMetadataProvider.
Upvotes: 3