Search code examples
asp.net-mvcasp.net-mvc-4model-view-controllerrazordisplayattribute

How to customize Display and Required attributes at run-time in MVC


I would like to have a model with a dynamic label in the razor view that gets set at runtime but is based on a string from a resource file using string formatting.

Lets say I have a simple model with a single property

public class Simple
{
    [Display(ResourceType = (typeof(Global)), Name = "UI_Property1")]
    [Required(ErrorMessageResourceType = (typeof(Global)), ErrorMessageResourceName = "ERROR_Required")]
    [StringLength(40, ErrorMessageResourceType = (typeof(Global)), ErrorMessageResourceName = "ERROR_MaxLength")]
    public string Property1{ get; set; }
}

And the resource file has the following strings

UI_Property1       {0}
ERROR_Required     Field {0} is required.
ERROR_MaxLength    Maximum length of {0} is {1}

and I would like to do something like this in the razor view

@Html.LabelFor(m => m.Property1, "xyz", new { @class = "control-label col-sm-4" })

and the resulting view would show the field label as 'xyz' and the value 'xyz' would also be shown in the validation messages returned from the server model validation.

I have been looking at various ways of doing this with no luck. I have investigated overriding the DisplayAttribute but this is a sealed class.

I also looked at overriding the DisplayName attribute but this does not get picked up properly with the required validation messages. Plus I wasn't sure how to inject the dynamic text in to the attribute which I assume will need to be done in the attribute constructor.

I have also looked at writing a custom DataAnnotationsModelMetadataProvider but cannot see a way of using this to achieve what I want. This may be down to my lack of coding skills.

The 'xyz' string will come from a setting in the web.config file and does not need to be injected at the LabelFor command but can be injected somewhere else if it would make more sense.

If anyone can give me clue as to how I might achieve this that would be great.


Solution

  • I found this post

    Is it valid to replace DataAnnotationsModelMetadataProvider and manipulate the returned ModelMetadata

    which led me to a solution as follows:

    I added a custom section to my web config

      <configSections>
        <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
        <section name="labelTranslations" type="AttributeTesting.Config.LabelTranslatorSection" />
        ... other sections here
      </configSections>
    
      <labelTranslations>
        <labels>
          <add label=":Customer:" translateTo="Customer Name" />
          <add label=":Portfolio:" translateTo="Portfolio Name" />
          <add label=":Site:" translateTo="Site Name" />
        </labels>
      </labelTranslations>
    

    The class for handling the custom section loads the labels that are to be translated

    public class LabelElement : ConfigurationElement
    {
        private const string LABEL = "label";
        private const string TRANSLATE_TO = "translateTo";
    
        [ConfigurationProperty(LABEL, IsKey = true, IsRequired = true)]
        public string Label
        {
            get { return (string)this[LABEL]; }
            set { this[LABEL] = value; }
        }
    
        [ConfigurationProperty(TRANSLATE_TO, IsRequired = true)]
        public string TranslateTo
        {
            get { return (string)this[TRANSLATE_TO]; }
            set { this[TRANSLATE_TO] = value; }
        }
    
    }
    
    [ConfigurationCollection(typeof(LabelElement))]
    public class LabelElementCollection : ConfigurationElementCollection
    {
        protected override ConfigurationElement CreateNewElement()
        {
            return new LabelElement();
        }
    
        protected override object GetElementKey(ConfigurationElement element)
        {
            return ((LabelElement)element).Label;
        }
    
        public LabelElement this[string key]
        {
            get
            {
                return this.OfType<LabelElement>().FirstOrDefault(item => item.Label == key);
            }
        }
    }
    
    public class LabelTranslatorSection : ConfigurationSection
    {
        private const string LABELS = "labels";
    
        [ConfigurationProperty(LABELS, IsDefaultCollection = true)]
        public LabelElementCollection Labels
        {
            get { return (LabelElementCollection)this[LABELS]; }
            set { this[LABELS] = value; }
        }
    }
    

    The translator then uses the custom section to translate a given label to the translated version if it exists otherwise it returns the label

    public static class Translator
    {
        private readonly static LabelTranslatorSection config =
            ConfigurationManager.GetSection("labelTranslations") as LabelTranslatorSection;
    
        public static string Translate(string label)
        {
            return config.Labels[label] != null ? config.Labels[label].TranslateTo : label;
        }
    }
    

    I then wrote a custom Metadata provider which modifies the displayname based on the translated version

    public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
    {
        protected override ModelMetadata CreateMetadata(
                                 IEnumerable<Attribute> attributes,
                                 Type containerType,
                                 Func<object> modelAccessor,
                                 Type modelType,
                                 string propertyName)
        {
    
            // Call the base method and obtain a metadata object.
            var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
    
            if (containerType != null)
            {
                // Obtain informations to query the translator.
                //var objectName = containerType.FullName;
                var displayName = metadata.GetDisplayName();
    
                // Update the metadata from the translator
                metadata.DisplayName = Translator.Translate(displayName);
            }
    
            return metadata;
        }
    }
    

    after that it all just worked and the labels and the validation messages all used the translated versions. I used the standard LabelFor helpers without any modifications.

    The resource file looks like this

    ERROR_MaxLength   {0} can be no more than {1} characters long   
    ERROR_Required    {0} is a required field   
    UI_CustomerName   :Customer:    
    UI_PortfolioName  :Portfolio:   
    UI_SiteName       :Site: