Мой первый «коммерческий» проект на ASP.Net MVC — небольшой сервис, ориентированный на русскоговорящую аудиторию. В этой заметке я хотел бы собрать проблемы, с которыми я столкнулся в процессе «русификации» MVC3 — то есть адаптации к российской локали и русификация интерфейса и сообщений об ошибках.
Проблемы, описываемые в этом посте:
- Указание культуры, использующейся по-умолчанию в байндингах
- Создание кастомного байндера
- Русификация сообщений от DataAnnotations-атрибутов
- Русификация сообщений от дефолтного байндера
- Проблемы интеграции локализации и Ninject
Начнем с простого: как известно, даты в разных странах принято писать по разному. И если в России 1.10.2011 — это 1 октября, то, например, в США — это 10 января. Похожая проблема и с дробными числами: 1,025 — в России это чуть больше единицы, а в США — 1025 (десятичный разделитель в США — точка).
Чтобы MVC-шный байндер при обработке пользовательского ввода правильно обрабатывал даты/числа, нужно в web.config указать нужную вам культуру:
<system.web> <globalization culture="ru-ru" /> </system.web>
Однако, байндер «подхватит» эту культуру только в случае POST-запросов. В случае GET-запросов всегда используется InvariantCulture. То есть если у вас на сайте форма, в которой есть дата и которая отправляется методом GET — ждите проблем. При вводе «15.10.2010» вы получите ошибку (байндер будет считать, что 15 — номер месяца).
К счастью, в MVC3 есть множество точек расширения, и ничего не мешает нам написать наш собственный байндер, который во всех случаях будет использовать CurrentCulture:
public class DateTimeModelBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); var modelState = new ModelState { Value = valueResult }; object actualValue = null; try { actualValue = DateTime.Parse(valueResult.AttemptedValue, CultureInfo.CurrentCulture); } catch (FormatException e) { modelState.Errors.Add(e); } bindingContext.ModelState.Add(bindingContext.ModelName, modelState); return actualValue; } } //изменять в файле Global.asax: protected void Application_Start() { //... //регистрация байндера ModelBinders.Binders.Add(typeof(DateTime), new DateTimeModelBinder()); }
Следующая проблема, которая у меня возникла — это локализация сообщений об ошибках.
Наверное, все знают о достаточно удобном механизме валидации вью-моделей с помощью DataAnnotation-атрибутов типа [Required], [RegularExpression] и других.
У них у всех можно, конечно, задавать сообщение об ошибке типа: [Required(ErrorMessage = «Поле необходимо заполнить!»)]. Но задавать это сообщение для каждого атрибута — это какое же дублирование получится! В решении этой проблемы мне помогла статья jgauffin, у которого я и утащил с небольшими правками реализацию локализации.
Интересующиеся могут почитать исходную статью, а менее любопытные просто утянуть готовый проект, в котором локализация выделена в отдельную небольшую библиотеку (с уже локализованными атрибутами Required, RegularExpression, StringLength и др.).
Интеграция локализаций занимает ровно одну строчку:
protected void Application_Start() { //... LocalizationIntegration.LocalizeDataAnnotationErrors(); }
Однако, DataAnnotation-атрибуты — это не единственный источник сообщений об ошибках. Если model-binder не может преобразовать строку «blablabla» введенную в поле для int-значения, он выдаст сообщение: «The value ‘blablabla’ is not valid for Int.». Локализовать это сообщение можно следующим образом:
protected void Application_Start() { //... DefaultModelBinder.ResourceClassKey = "Messages_Ru"; }
При этом нужно создать ресурс с именем Messages_Ru и положить его в папку App_GlobalResources вашего MVC-проекта. В этом ресурсе необходимы, в общем-то, только два ключа-значения:
PropertyValueInvalid Неверное значение '{0}' для поля {1} PropertyValueRequired Не заполнено обязательное поле {0}
В тестовом проекте это тоже присутствует.
При интеграции всего вышеупомянутого с IoC-контейнером Ninject (а точнее, с Ninject.MVC3 — nuget-пакетом, предоставляющим интеграцию) возникли небольшие проблемы, из-за того, что Ninject переопределяет DataAnnotationsModelValidatorProvider, чтобы проводить инъекцию зависимостей в DataAnnotation-атрибуты.
Эта проблема решена классами LocalizationIntegration и NinjectLocalizedDataAnnotationsModelValidatorProvider в тестовом проекте, но суть изменений сводится к следующему:
protected void Application_Start() { //регистрация валидатора в проектах без Ninject.MVC3 var localizedStringProvider = new ResourceStringProvider(Resources.ResourceManager); ModelValidatorProviders.Providers.Clear(); ModelValidatorProviders.Providers.Add(new LocalizedModelValidatorProvider(localizedStringProvider)); //регистрация валидатора в проектах с Ninject.MVC3 bootstrapper.Kernel.Rebind<ModelValidatorProvider>().ToConstant(new LocalizedModelValidatorProvider(localizedStringProvider)); //в процессе инициализации Ninject.MVC3 зарегистрирует свою реализацию ModelValidatorProvider, поэтому необходимо произвести Rebind //в тестовом проекте класс NinjectLocalizedDataAnnotationsModelValidatorProvider совмещает в себе преимцщества локализации и инъекции зависимостей }
Данаая реализация отлично подходит для быстрого решения повседневных задач с минимумом интервенции в существующую систему. Интересующимся рекомендую также ознакомиться с проектом MvcExtensions, который решает эту (и другие) проблемы более комплексно. Я также надеюсь в ближайшее время ознакомиться с ним более детально и, возможно, написать о процессе знакомства :)
P.S. Тестовый проект с реализацией всего вышеупомянутого: DataAnnotation.zip