Как локализовать имена полей модели без атрибута [Display]? - PullRequest
0 голосов
/ 11 июля 2019

Я реализовал локализацию для атрибутов проверки с использованием подхода IValidationMetadataProvider.

Используются имена полей модели в сообщениях об ошибках.

Я хотел бы перевести имена полей из строк ресурсов, так же, как я делаю для сообщений. Но я не хочу помещать атрибут [Display("FieldName")] в каждое поле. Нет необходимости ставить даже пустой атрибут [Display] - это будет избыточный шаблонный код.

В идеале, я бы хотел как-то сообщить валидатору MVC, что каждый раз, когда ему требуется имя поля для сообщения валидации, он должен запрашивать его у какого-то собственного провайдера, чтобы я мог вернуть значение из моей реализации IStringLocalizer.

Можно ли как-нибудь передать имена пользовательских полей в валидатор MVC, не разбрасывая атрибут [Display] везде?

1 Ответ

0 голосов
/ 12 июля 2019

После долгих проб и ошибок, а также поиска инъекций DisplayAttribute на основе отражений я обнаружил, что возможно внедрить свой собственный IDisplayMetadataProvider.

Итак, теперь я могу полностью опустить атрибут [Display] и мой классавтоматически извлечет имена полей из ресурсов, соответствующие именам свойств модели.Дополнительный бонус - он обеспечит непротиворечивые имена для совпадающих свойств, потому что он будет использовать глобальные значения по умолчанию, если к конкретному свойству не применен явный [Display(Name = "SomeOtherResourceKey")].


public class LocalizableInjectingDisplayNameProvider : IDisplayMetadataProvider
    {
        private IStringLocalizer _stringLocalizer;
        private Type _injectableType;

        public LocalizableInjectingDisplayNameProvider(IStringLocalizer stringLocalizer, Type injectableType)
        {
            _stringLocalizer = stringLocalizer;
            _injectableType = injectableType;
        }

        public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
        {
            // ignore non-properties and types that do not match some model base type
            if (context.Key.ContainerType == null || 
                !_injectableType.IsAssignableFrom(context.Key.ContainerType))
                return;

            // In the code below I assume that expected use of field name will be:
            // 1 - [Display] or Name not set when it is ok to fill with the default translation from the resource file
            // 2 - [Display(Name = x)]set to a specific key in the resources file to override my defaults

            var propertyName = context.Key.Name;
            var modelName = context.Key.ContainerType.Name;

            // sanity check 
            if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
                return;

            var fallbackName = propertyName + "_FieldName";
            // If explicit name is missing, will try to fall back to generic widely known field name,
            // which should exist in resources (such as "Name_FieldName", "Id_FieldName", "Version_FieldName", "DateCreated_FieldName" ...)

            var name = fallbackName;

            // If Display attribute was given, use the last of it
            // to extract the name to use as resource key
            foreach (var attribute in context.PropertyAttributes)
            {
                var tAttr = attribute as DisplayAttribute;
                if (tAttr != null)
                {
                    // Treat Display.Name as resource name, if it's set,
                    // otherwise assume default. 
                    name = tAttr.Name ?? fallbackName;
                }
            }

            // At first, attempt to retrieve model specific text
            var localized = _stringLocalizer[name];

            // Final attempt - default name from property alone
            if (localized.ResourceNotFound)
                localized = _stringLocalizer[fallbackName];

            // If not found yet, then give up, leave initially determined name as it is
            var text = localized.ResourceNotFound ? name : localized;

            context.DisplayMetadata.DisplayName = () => text;
        }

    }

...