Пользовательский связыватель модели 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 и обратно.