Custom DateTime model binder in Asp.net MVC
Asked Answered
S

5

25

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

  • implement IModelBinder interface and write my own BindModel() method
  • inherit from DefaultModelBinder and override BindModel() method

My 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?

Edit

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.

Soloist answered 1/3, 2010 at 14:52 Comment(5)
does this help? hanselman.com/blog/…Baseline
not really, because he doesn't use any custom attributes. I could use BindAttribute but that's not a universal solution. You could easily forget writing that in your action.Soloist
Have you got a working solution for this problem? I have the same issue and I would like to know which solution did you pickEnthusiastic
@Davide Vosti: I ended up reformating datetime value on the client into a hidden field. It got populated when user blurs from the date selection field. And it works. It's a workaround, it doesn't come with lots of additinal code, and it works in my scenario.Soloist
thanks! In the mean time I was able to find a good solution. Thanks anyway for your suggestionEnthusiastic
T
3

I don't think you should put locale-specific attributes on a model.

Two other possible solutions to this problem are:

  • Have your pages transliterate dates from the locale-specific format to a generic format such as yyyy-mm-dd in JavaScript. (Works, but requires JavaScript.)
  • Write a model binder which considers the current UI culture when parsing dates.

To answer your actual question, the way to get custom attributes (for MVC 2) is to write an AssociatedMetadataProvider.

Tussah answered 1/3, 2010 at 15:22 Comment(6)
It's not really related to locale-specific formatting. The problem is that DateTime must have both date and time in a string for default binder to parse it correctly. Doesn't matter which localisation is used. I just want to provide date from the client and parse it correctly to a DateTime (time set to 00:00:00) instance on model binding.Soloist
If at all possible I'd like to avoid writing custom metadata provider. but I guess this could be exactly what I need. I could attach my own attributes to ModelMetadata information.Soloist
It is not true that DateTime.Parse requires a string. Try var dt = DateTime.Parse("2010-03-01"); I guarantee it works! A particular DateTime format might, though.Tussah
DateTime.parse may be fine with that, DefaultModelBinder obviously isn't. My date format is the same as defined by locale anyway. I tried loading a view model with date time and displaying a strong type view that consumes it and displayed date including time. Nad when I include time in my DateTime property everything works fine. Otherwise I gave validation error (using DataAnnotations)Soloist
I wouldn't be so sure that "DefaultModelBinder obviously isn't." It works fine for that, at least here. I note you're in Slovenia, so it's possible (though not obvious!) that a machine in a certain configuration won't parse yyyy-mm-dd, although that should work in any culture. But returning to the point at hand, an associated metadata provider is all of 20 lines of code or so, and will supply your binder the info you want.Tussah
Based on metadata provider I accept your answer as the most appropriate solution. Thanks Craig.Soloist
N
89

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.

Nakisha answered 7/11, 2011 at 11:11 Comment(8)
thanks, this way worked best. I see other replies may work proper too, but here if we add this below instead of the value.convertTo, the datetime format cam be controlled wihthout bothering the culture, because we set up the format for the fornt end too (jQueryUI datepicker for example). ->>> var dateFormat = bindingContext.ModelMetadata.EditFormatString; return DateTime.ParseExact(((string[]) value.RawValue)[0], dateFormat, CultureInfo.InvariantCulture);Rescind
Tip of the hat to you sir.Fanniefannin
Adding a binder seems to break Url.Action. It no longer uses correct culture when I pass DateTime into it. It seems to use en-US instead.Titanate
@KamilSzot, I see no reason why it would break it, open a question with all the details and leave a link here please.Nakisha
@Nakisha My mistake. Url.Action always seems to use InvariantCulture for generating query string passed DateTime route values, regardless of binders.Titanate
@KamilSzot, OK, you can delete the comments if so.Nakisha
I think that implementation of NullableDateTimeBinder is suitable for not null DateTime too. And we can leave only NullableDateTimeBinder class.Terylene
You should also add the field to the ModelState, otherwise HTML input helpers it won't be able to use it to repopulate itself (ie. when the page is redisplayed due to a validation error) and you'll have empty fields: bindingContext.ModelState.Add(bindingContext.ModelName, new ModelState { Value = value });Convenance
E
14

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 :(

Enthusiastic answered 11/6, 2010 at 6:33 Comment(0)
T
3

I don't think you should put locale-specific attributes on a model.

Two other possible solutions to this problem are:

  • Have your pages transliterate dates from the locale-specific format to a generic format such as yyyy-mm-dd in JavaScript. (Works, but requires JavaScript.)
  • Write a model binder which considers the current UI culture when parsing dates.

To answer your actual question, the way to get custom attributes (for MVC 2) is to write an AssociatedMetadataProvider.

Tussah answered 1/3, 2010 at 15:22 Comment(6)
It's not really related to locale-specific formatting. The problem is that DateTime must have both date and time in a string for default binder to parse it correctly. Doesn't matter which localisation is used. I just want to provide date from the client and parse it correctly to a DateTime (time set to 00:00:00) instance on model binding.Soloist
If at all possible I'd like to avoid writing custom metadata provider. but I guess this could be exactly what I need. I could attach my own attributes to ModelMetadata information.Soloist
It is not true that DateTime.Parse requires a string. Try var dt = DateTime.Parse("2010-03-01"); I guarantee it works! A particular DateTime format might, though.Tussah
DateTime.parse may be fine with that, DefaultModelBinder obviously isn't. My date format is the same as defined by locale anyway. I tried loading a view model with date time and displaying a strong type view that consumes it and displayed date including time. Nad when I include time in my DateTime property everything works fine. Otherwise I gave validation error (using DataAnnotations)Soloist
I wouldn't be so sure that "DefaultModelBinder obviously isn't." It works fine for that, at least here. I note you're in Slovenia, so it's possible (though not obvious!) that a machine in a certain configuration won't parse yyyy-mm-dd, although that should work in any culture. But returning to the point at hand, an associated metadata provider is all of 20 lines of code or so, and will supply your binder the info you want.Tussah
Based on metadata provider I accept your answer as the most appropriate solution. Thanks Craig.Soloist
P
1

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;
    }
}
Pasticcio answered 4/7, 2023 at 15:47 Comment(0)
B
0

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.

Brigade answered 5/9, 2013 at 7:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.