4

I'm working on a legacy application build with ASP.NET Dynamic Data. The models, as per usual, are all read-only and one can set the display name or description through attributes.

This worked well, however, now I'm in a situation where I need to query two different sources (resource file and some other source) for display name.

The code before was clean, because we only queried the resources:

[Display(ResourceType = typeof(Resources.m_Res), Name = "M_RES_MIMETYPE_ID", Description = "M_RES_MIMETYPE_ID_DESCR")]

This was totally fine and it worked as intended. However, now I have to get the display name and description firstly from some other file and if everything else fails, I have to fallback to resources.

I had to create two different custom attributes, something in this manner:

    public class MGADisplayName : DisplayNameAttribute
    {
          private readonly string propertyName;
          public string Name { get; set; }
          public Type TableName { get; set; }
          public Type ResourceType { get; set; }

          public MGADisplayName([CallerMemberName] string PropertyName = null)
          {
              propertyName = PropertyName;
          }

          public override string DisplayName
          {
              get
              {
                  var key = (TableName.Name + ":" + (propertyName ?? Name)).ToLower();
                  if (/* SOME QUERYING */)
                  {
                      return QUERY[key];
                  }
                  else
                  {
                      string property = Resources.m_Res.ResourceManager.GetString(Name);
                      if (property != null)
                      {
                          return property;
                      }
                      else
                      {
                          return Name;
                      }

                  }
              }
          }
    }

This kind of works and I guess it's OK for the time being, but the next issue is around the corner: I'll need to do with Display.GroupName the same.

Now, as far as I know, there is no GroupNameAttribute to extend, so I'm kind of in the dark here.

I wish I could extend the DisplayAttribute, it would be EXACTLY what I need, but the class is sealed, so that's a dead end.

I wish I could change the model on the fly and provide DisplayName and Description through setters, but the model has only getters, so that's another dead end.

I'm running slowly out of options here. What else can be done here?

uglycode
  • 2,762
  • 4
  • 23
  • 50
  • I didn't realize that why you insist on extending pre-defined attributes of .net and not creating completely new ones??! – Arman Ebrahimpour May 19 '20 at 10:29
  • Because I'm using a model that is created by the .net. Meaning that GroupName and Description are used by every .aspx template by default. I could create a custom MyGroupNameAttribute, but the GroupName value attribute would be ignored then. – uglycode May 19 '20 at 11:01
  • Would it help to add the attribute at runtime? – Michael May 20 '20 at 22:49
  • At this point, I'm not excluding anything:) What did you have in mind? – uglycode May 21 '20 at 06:11

2 Answers2

0

Although DisplayAttribute class is sealed, it can be customized via ResourceType property:

Gets or sets the type that contains the resources for the ShortName, Name, Prompt, and Description properties.

Two things to mention. First, it also handles GroupName. Second is mentioned in the Remarks section:

If this value is not null, the string properties are assumed to be the names of public static properties that return the actual string value.

Shortly, the provided type can be any public class / struct having public static string property for each string key.

This perfectly fits with PublicResXFileCodeGenerator generated typed resource classes. But the important is that it can be arbitrary type, and you can utilize that fact.

For instance:

public static class SR
{
    // Assuming you have file SR.resx under same namespace
    static readonly ResourceManager ResourceManager = new ResourceManager(typeof(SR));

    static string GetString([CallerMemberName]string propertyName = null)
    {
        // Custom logic goes here
        return ResourceManager.GetString(propertyName) ?? propertyName;
    }

    // Properties
    public static string M_RES_MIMETYPE_ID => GetString();
    public static string M_RES_MIMETYPE_ID_DESCR => GetString();
    public static string M_RES_MIMETYPE_ID_GROUP => GetString();
    // ...
}

Usage:

[Display(
    ResourceType = typeof(SR), 
    Name = nameof(SR.M_RES_MIMETYPE_ID),
    Description = nameof(SR.M_RES_MIMETYPE_ID_DESCR),
    GroupName = nameof(SR.M_RES_MIMETYPE_ID_GROUP)
)]

Test:

var displayInfo = new DisplayAttribute
{
    ResourceType = typeof(SR),
    Name = nameof(SR.M_RES_MIMETYPE_ID),
    Description = nameof(SR.M_RES_MIMETYPE_ID_DESCR),
    GroupName = nameof(SR.M_RES_MIMETYPE_ID_GROUP)
};
// These are used by ASP.NET for retrieving actual values
var name = displayInfo.GetName();
var description = displayInfo.GetDescription();
var groupName = displayInfo.GetGroupName();

So, this is the solution.

Now, the only problem with it is the need to manually add string property for each resource key. It could be solved by rolling out your own single file generator or T4 template generator, but how to do that I believe is out of the scope of this post.

Ivan Stoev
  • 159,890
  • 9
  • 211
  • 258
-1

As you mentioned as well DisplayAttribute is sealed. And for custom scenarios we need to add our own custom attribute. But to simplify implementation for custom attribute we can use wrapper pattern to reuse original code as well as adding our own custom code. Like this:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = false)]
    public class CustomDisplayAttribute : Attribute
    {
        private DisplayAttribute _innerAttribute;

        public CustomDisplayAttribute()
        {
            _innerAttribute = new DisplayAttribute();
        }

        public string ShortName
        {
            get
            {
                return _innerAttribute.ShortName;
            }
            set
            {
                _innerAttribute.ShortName = value;
            }
        }
...

The custom added attribute would not be picked as it does not implement DisplayAttribute. To have it be picked on reading metadata, we need to add a custom meta data provider. It should inherits from DataAnnotationsModelMetadataProvider. You can customize the provider code as you wish.

public class CustomDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider
    {
        protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
        {
            var result = base.CreateMetadata(attributes, containerType,
                    modelAccessor, modelType, propertyName);

            CustomDisplayAttribute customDisplay = attributes.OfType<CustomDisplayAttribute>().FirstOrDefault();
            if (customDisplay != null)
            {
                result.Description = customDisplay.GetDescription();
                result.ShortDisplayName = customDisplay.GetShortName();
                result.Watermark = customDisplay.GetPrompt();
                result.Order = customDisplay.GetOrder() ?? ModelMetadata.DefaultOrder;

                result.DisplayName = customDisplay.GetName();
            }

            return result;
        }
    }

And at the end to make sure CustomDataAnnotationsModelMetadataProvider is used, we should set the current meta data provider to be it. We can add this code on Global.asax.cs or Startup.cs to set the current value.

ModelMetadataProviders.Current = new CustomDataAnnotationsModelMetadataProvider();

Find the complete sample code here. I tested it on an MVC web app in same directory. Feel free to clone the repo and try it out.

BTW as mentioned on the other answer this solution was discussed long time ago on here, I just tried to make it prettier and added full sample code

nahidf
  • 1,715
  • 1
  • 10
  • 14
  • This seems like the solution I need and I'm willing to re-open the bounty and accept your answer, however, I'm not sure if this works on Web Forms as well. I get the "The name 'ModelMetadataProviders' does not exist in the current context" error when doing this: `ModelMetadataProviders.Current = new CustomDataAnnotationsModelMetadataProvider();` – uglycode May 28 '20 at 11:04
  • @uglycode yes it sure works on MVC, I have sample on my repo shared. For WebForms, how do you use it? can you share code, just need to replicate the scenario. – nahidf May 28 '20 at 16:35
  • I'm using it inside the `void Application_Start(object sender, EventArgs e)`, basically the same way as you do in your repo. I'm not sure if `ModelMetadataProviders` is even part of web forms, i.e., I'm not sure if this solution can be applied? – uglycode May 29 '20 at 06:12
  • @uglycode I'll try to do a POC, needs a bit of time, as I'm no .NET forms pro, will get back to you if I succeed. – nahidf May 29 '20 at 13:35
  • Thanks, I'd appreciate that, because I fear that it won't be possible to do that in web forms:/ sadly... – uglycode Jun 01 '20 at 06:29
  • @uglycode sorry I couldn't figure how to do it in webforms :( – nahidf Jun 12 '20 at 19:07
  • No problem, man. I guess that Ivan Stoev's answer is the closest to the solution. Although I managed to manually change the Display's attributes of an property, so that's still an option. – uglycode Jun 15 '20 at 06:20