Пользовательский связыватель модели DateTime в Asp.net MVC

Я хотел бы написать свою собственную модель для переплета DateTime тип. Прежде всего я хотел бы написать новый атрибут, который я могу прикрепить к свойству модели, например:

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

Это легкая часть. Но часть связующего немного сложнее. Я хотел бы добавить новую модель связующего для типа DateTime, Я могу либо

  • воплощать в жизнь IModelBinder интерфейс и написать свой собственный BindModel() метод
  • наследовать от DefaultModelBinder и переопределить BindModel() метод

Моя модель имеет свойство, как показано выше (Birth). Поэтому, когда модель пытается связать данные запроса с этим свойством, моя модель связывания BindModel(controllerContext, bindingContext) вызывается. Все хорошо, но. Как я могу получить атрибуты свойства из controller/bindingContext, чтобы правильно проанализировать мою дату? Как я могу добраться до PropertyDesciptor собственности Birth?

редактировать

Из-за разделения интересов мой класс модели определен в сборке, которая не (и не должна) ссылаться на сборку System.Web.MVC. Настраивать пользовательские привязки (аналогично примеру Скотта Хансельмана) здесь не нужно.

5 ответов

Решение

Я не думаю, что вы должны поместить специфичные для локали атрибуты в модель.

Два других возможных решения этой проблемы:

  • Сделайте так, чтобы ваши страницы транслитерировали даты из специфичного для локали формата в общий формат, такой как гггг-мм-дд в JavaScript. (Работает, но требует JavaScript.)
  • Напишите подшивку модели, которая учитывает текущую культуру пользовательского интерфейса при разборе дат.

Чтобы ответить на ваш реальный вопрос, способ получить пользовательские атрибуты (для MVC 2) - написать AssociatedMetadataProvider.

Вы можете изменить связыватель модели по умолчанию для использования пользовательской культуры с помощью 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 Framework применила стандартную культуру для всех пользователей.

У меня была эта очень большая проблема, и после нескольких часов попыток и неудач я получил рабочее решение, как вы и просили.

Прежде всего, поскольку привязка только к свойству невозможна, вам необходимо реализовать полный 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);
        }
    }
}

В моем примере я разбираю свидание с жестокой культурой, но то, что вы хотите сделать, возможно. Вы должны создать CustomAttribute (например, DateTimeFormatAttribute) и поместить его поверх своего свойства:

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

Теперь в методе BindProperty вместо поиска свойства DateTime вы можете искать свойство с вашим DateTimeFormatAttribute, получить формат, указанный вами в конструкторе, а затем проанализировать дату с помощью DateTime.ParseExact.

Надеюсь, это поможет, мне понадобилось очень много времени, чтобы прийти с этим решением. Было действительно легко найти это решение, когда я знал, как его искать:(

Для 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;
    }
}

Вы можете реализовать пользовательский механизм связывания DateTime таким образом, но вы должны позаботиться о предполагаемой культуре и значении фактического запроса клиента. Можете ли вы получить дату типа mm/dd/yyyy в en-US и захотите, чтобы она конвертировалась в системную культуру 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 в зависимости от культуры в приложении без состояния может быть жестоким... Особенно, когда вы работаете с JSON на стороне клиента javascript и обратно.

Другие вопросы по тегам