4

I have the following method where I read from a key-value XML file. I pass in a key and am returned a value where I used to display on my view.

public static class TextManager
{
    public static string GetValue(string key)
    {
        string returnVal = null; 
        XmlSerializer serializer = new XmlSerializer(typeof(Entries));
        string path = HttpContext.Current.Server.MapPath("/App_Data/text-key-value.xml");
        if (File.Exists(path))
        {
            Entries entries = (Entries)serializer.Deserialize(File.OpenRead(path));
            var entry = entries.Where(u => u.Key == key).FirstOrDefault();
            if (entry != null)
            {
                returnVal = entry.Value;
            }
        }
        return returnVal;
    }
}

Basically I want to be able to use this method in my model class as a data-annotation that will pull directly from my site text file and set to the display name property.

For instance I want to replace

[Display(Name = "Reference Code")]
public string ReferenceCode { get; set; }

With this

[DisplaySiteText("ReferenceCodeKey")]
public string ReferenceCode { get; set; }

DisplaySiteText would pass the string reference "ReferenceCodeKey" to the GetValue method, file the reference in the file and then set the standard Display name attribute to whatever was in the file.

How do I create my own custom model annotation to do this, I've written custom validation annotations in the past by creating a class that inherits from ValidationAttribute, but I don't think that will work in this case.

TroySteven
  • 4,885
  • 4
  • 32
  • 50
  • 2
    I think you would just need a regular attribute (as opposed to a ValidationAttribute). `DisplaySiteText : Attribute` to define the key, and then use a extension method to get the value of the key and returning the `TextManager.GetValue` call – Andy Stagg Aug 08 '19 at 15:45
  • 1
    That works and is a start but then how exactly do I set the value of display name from within the DisplaySiteText call? – TroySteven Aug 08 '19 at 15:46
  • 1
    The same way that you can create new `ValidationAttribute`, you can create new metadata attribute by deriving from `IMetadataAware` interface. You also have an option to use existing attributes by creating a new metadata provider by deriving from `DataAnnotationsModelMetadataProvider`. I've described the options in the [answer](https://stackoverflow.com/questions/57416242/override-mvc-model-display-name-annotation-with-custom-functionality#57448252). – Reza Aghaei Aug 11 '19 at 12:04

3 Answers3

7

You can inherit DisplayNameAttribute for this purpose

public class DisplaySiteTextAttribute : DisplayNameAttribute
{
    private string _key;

    public DisplaySiteTextAttribute(string key)
    {
        _key = key;
    }

    public override string DisplayName
    {
        get
        {
            return TextManager.GetValue(_key);
        }
    }
}
Alexander
  • 9,104
  • 1
  • 17
  • 41
  • You have your constructor named incorrectly, it should be named DisplaySiteTextAttribute instead of ResourceDisplayAttribute. – TroySteven Aug 12 '19 at 13:56
6

There are several options to customize model metadata:

  • Customize the way that framework provides metadata. (Create ModelMedatadaProvider)
  • Create new Metadata attributes. (Implement IMetadataAware)
  • Modify existing attributes. (Derive existing attributes.)

The 3rd option has been discussed in the other answer. Here in this post, I'll share first and second options.

Option 1 - Customize the way that framework provides metadata

You can change the logic of getting display text without changing the attribute.

In fact it's responsibility of ModelMetaDataProvider to get mete data for model, including display text for properties. So as an option, you can keep the Display attribute intact and instead, create a new model metadata provider and return property metadata from a different source.

To do so, you can create a new metadata provider by deriving from DataAnnotationsModelMetadataProvider. Then override GetMetadataForProperty and call base, to get metadata. Then change DisplayName based on your logic by reading from your text manager.

You also need to register the new metadata provider as ModelMetadataProviders.Current in App_Start.

using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
public class MyCustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor,
        Type containerType,
        PropertyDescriptor propertyDescriptor)
    {
        var metadata = base.GetMetadataForProperty(modelAccessor, 
            containerType, propertyDescriptor);
        var display = propertyDescriptor.Attributes
            .OfType<DisplayAttribute>().FirstOrDefault();
        if (display != null)
        {
            metadata.DisplayName = TextManager.GetValue(display.Name);
        }
        return metadata;
    }
}

And then register it in Application_Start():

ModelMetadataProviders.Current = new MyCustomModelMetadataProvider();

For more information take a look at DataAnnotationsModelMetadataProvider.cs source code in ASP.NET MVC sources.

This approach is useful when you want to change the way that you provide metadata for model. For example when you want to load display name and description from an external file rather than resources, without changing existing attributes.

Option 2 - Create new Metadata attributes

Another standard solution for creating metadata-aware attributes is creating an attribute and implementing IMetadataAware interface. Then in implementation of OnMetadataCreated you can easily set properties of metadata.

This approach doesn't need to register a new meta data provider. This approach is supported by the default metadata provider and is useful for creating new metadata-aware attributes:

using System;
using System.Web.Mvc;
public class CustomMetadataAttribure : Attribute, IMetadataAware
{
    public string Key { get; set; }
    public CustomMetadataAttribure(string key) => this.Key = key;
    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.DisplayName = TextManager.GetValue(this.Key);
    }
}

This approach is useful when you want to extend metadata attributes and add a few more attributes. For example when you want to add some attributes to control rendering. You can set ModelMetadata properties or add some new values to its AdditionalValues dictionary.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
1

Maybe your DisplaySiteText attribute could inherit from the Display attribute and set the name using your helper class. Something like this:

public class DisplaySiteTextAttribute : DisplayAttribute
{
    public DisplaySiteTextAttribute(string key)
    {
        Name = TextManager.GetValue(key);
    }
}
Mun
  • 14,098
  • 11
  • 59
  • 83