在Asp.net MVC中自定义DateTime模型绑定器

25

我想为DateTime类型编写自己的模型绑定器。首先,我想编写一个新的属性,可以将其附加到我的模型属性上:

[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}

这是简单的部分。但绑定器部分有点更加困难。我想为类型DateTime添加一个新的模型绑定器。我可以:

  • 实现IModelBinder接口并编写自己的BindModel()方法
  • 继承DefaultModelBinder并覆盖BindModel()方法

我的模型有如上所示的一个属性(Birth)。因此,当模型尝试将请求数据绑定到该属性时,我的模型绑定器的BindModel(controllerContext, bindingContext)会被调用。一切正常,但是我怎样从控制器/绑定上下文中获取属性注释以正确解析我的日期?我如何获取属性BirthPropertyDesciptor

编辑

由于关注点的分离,我的模型类定义在一个不引用System.Web.MVC程序集的程序集中(也不应该引用)。在这里设置自定义绑定(类似于Scott Hanselman的例子)属性是行不通的。


这有用吗?http://www.hanselman.com/blog/SplittingDateTimeUnitTestingASPNETMVCCustomModelBinders.aspx - Marek
不完全是,因为他没有使用任何自定义属性。我可以使用BindAttribute,但这不是一个通用的解决方案。你很容易忘记在你的操作中写入它。 - Robert Koritnik
你已经有了解决这个问题的方案吗?我遇到了同样的问题,想知道你选择了哪个解决方案。 - Davide Vosti
@Davide Vosti:我最终在客户端将日期时间值重新格式化为隐藏字段。当用户从日期选择字段中模糊时,它会被填充。它可以工作。这是一个解决方法,不需要大量的额外代码,并且适用于我的情况。 - Robert Koritnik
谢谢!与此同时,我已经找到了一个好的解决方案。无论如何,还是感谢您的建议。 - Davide Vosti
5个回答

87

您可以使用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);
    }
}

在Global.asax中的Application_Start()方法中添加以下代码:

ModelBinders.Binders.Add(typeof(DateTime), new DateTimeBinder());
ModelBinders.Binders.Add(typeof(DateTime?), new NullableDateTimeBinder());

阅读这篇出色的博客了解为什么 Mvc 框架团队将默认文化应用于所有用户:点击查看


2
向您致敬,先生。 - TheGwa
@KamilSzot,我看不出它会出现问题的原因,请提出一个包含所有细节的问题,并在此处留下链接。 - gdoron
1
我的错误。无论绑定器如何,Url.Action似乎总是使用InvariantCulture来生成传递DateTime路由值的查询字符串。 - Kamil Szot
@KamilSzot,好的,如果可以的话,您可以删除注释。 - gdoron
1
我认为NullableDateTimeBinder的实现也适用于非空的DateTime。我们可以只保留NullableDateTimeBinder类。 - sDima
显示剩余3条评论

14

我自己曾经遇到过这个非常棘手的问题,在尝试了数个小时后终于得到了像你所要求的工作解决方案。

首先需要说明的是,由于仅在属性上设置绑定器并不可行,您必须实现一个完整的ModelBinder。 由于您只想绑定特定的单个属性,而不是所有属性,因此可以从DefaultModelBinder继承,然后将单个属性绑定:

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);
        }
    }
}

在我的示例中,我正在使用固定的区域设置解析日期,但你想要做的是可能的。你应该创建一个自定义属性(类似于DateTimeFormatAttribute),并将其放在你的属性上方:

[DateTimeFormat("d.M.yyyy")]
public DateTime Birth { get; set,}

在BindProperty方法中,不再查找DateTime属性,而是查找带有DateTimeFormatAttribute的属性,获取您在构造函数中指定的格式,然后使用DateTime.ParseExact解析日期。

希望这可以帮助你,我花了很长时间才想出这个解决方案。一旦知道如何搜索,实际上很容易得到这个解决方案 :(


3
我认为您不应该在模型上放置特定于区域设置的属性。
此问题的另外两个可能的解决方案是:
- 在JavaScript中将页面上的日期从特定于区域设置的格式转换为通用格式,例如yyyy-mm-dd。(有效,但需要JavaScript。) - 编写一个模型绑定器,在解析日期时考虑当前的UI文化。
要回答您实际的问题,获取自定义属性的方法(对于MVC 2)是编写一个关联元数据提供程序

这与特定于区域设置的格式无关。问题在于DateTime必须在字符串中同时包含日期和时间,以便默认绑定程序正确解析它。使用哪种本地化都无所谓。我只想从客户端提供日期并在模型绑定时将其正确解析为DateTime实例(时间设置为00:00:00)。 - Robert Koritnik
如果可能的话,我想避免编写自定义元数据提供程序。但我猜这可能正是我所需要的。我可以将自己的属性附加到ModelMetadata信息上。 - Robert Koritnik
DateTime.Parse 不一定需要一个字符串。尝试使用 var dt = DateTime.Parse("2010-03-01");,我保证它能正常工作!但是特定的 DateTime 格式 可能需要。 - Craig Stuntz
DateTime.parse可能可以处理这个问题,但DefaultModelBinder显然不能。我的日期格式与本地定义的相同。我尝试使用日期时间加载视图模型,并显示一个强类型视图来消耗它并显示包括时间的日期。当我在DateTime属性中包含时间时,一切都正常工作。否则,我会收到验证错误(使用DataAnnotations)。 - Robert Koritnik
我不会那么肯定地说 "DefaultModelBinder 显然不行。" 至少在这里它可以正常工作。我注意到你在斯洛文尼亚,所以有可能(虽然不是*显而易见的!)某种配置下的机器无法解析 yyyy-mm-dd,尽管这应该在任何文化中都能正常工作。但回到手头的问题,一个相关的元数据提供程序只需要大约20行代码左右,就可以为您的绑定器提供所需的信息。 - Craig Stuntz
基于元数据提供者,我认为您的答案是最合适的解决方案。谢谢Craig。 - Robert Koritnik

1
对于ASP.NET Core,您可以使用以下自定义模型绑定器。 下面给出了一个示例模型。
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; }
} 

DateTime值的自定义绑定器。它期望的格式为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;
    }
}

0
您可以像这样实现自定义的 DateTime Binder,但是您必须注意假定文化和实际客户端请求值。例如,您可能会在 en-US 中获得 mm/dd/yyyy 的日期,并希望将其转换为系统文化 en-GB(类似于 dd/mm/yyyy),或者使用不变的文化,就像我们一样,那么您必须先解析它,然后使用静态外观Convert更改其行为。
    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;
        }
    }

无论如何,在一个无状态的应用程序中进行文化依赖的DateTime解析可能是一种残酷的行为...特别是当你在javascript客户端和反向JSON上工作时。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接