JHW
JHW

Reputation: 124

Accessing custom attributes from model metadata in a tag helper

I am working on a new project in .net Core having previously been working with .net Framework.

I wish to produce html select elements for boolean properties but using custom values instead of True and False (mainly "Yes" and "No"). In previous projects I have used the following method:

Create a custom attribute for boolean values:

[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public class BooleanCustomValuesAttribute : Attribute, IMetadataAware
{
    public string TrueValue { get; set; }
    public string FalseValue { get; set; }

    public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.AdditionalValues["TrueValue"] = TrueValue;
        metadata.AdditionalValues["FalseValue"] = FalseValue;
    }
}

Use this in my model:

[BooleanCustomValues(TrueValue = "Yes", FalseValue = "No")]
public bool ThisWorks { get; set; }

Then in an editor template I can access these values for adding to a select:

object trueTitle;
ViewData.ModelMetadata.AdditionalValues.TryGetValue("TrueValue", out trueTitle);
trueTitle = trueTitle ?? "True";

object falseTitle;
ViewData.ModelMetadata.AdditionalValues.TryGetValue("FalseValue", out falseTitle);
falseTitle = falseTitle ?? "False";

I am now trying to do something similar in .net core, this time using a tag-helper instead of an editor template.

I understand that setting the AdditionalValues as above is not supported in core?

The closest I have found is this answer: Getting property attributes in TagHelpers

My tag helper code looks like this currenty (outputting p just for testing):

public class BooleanSelectTagHelper: TagHelper
    {
        [HtmlAttributeName("asp-for")]
        public ModelExpression Source { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = "p";
            output.TagMode = TagMode.StartTagAndEndTag;

            var booleanAttribute = Source.Metadata.ContainerType.GetProperty(Source.Name).GetCustomAttribute(typeof(BooleanCustomValuesAttribute));

            var contents = $@"{booleanAttribute.TrueValue} or {booleanAttribute.FalseValue}";

            output.Content.SetHtmlContent(new HtmlString(contents));
        }
    }

However I can't get it to work as Source.Metadata.ContainerType.GetProperty(Source.Name).GetCustomAttributes() returns null.

Anoyingly when debugging and looking at Source.Metadata in a watch window I can see exactly what I want to access under Source.Metadata.Attributes - however this is not something that seems to be exposed outside the debugger.

Apologies for length of post - any pointers would be much appreciated (including tellimg me I am doing this all wrong!)

Upvotes: 0

Views: 1970

Answers (2)

Mike
Mike

Reputation: 1683

Maybe this will help, this is what I use to add the attributes for unobtrusive validation when I create custom TagHelpers. You can adapt this to whatever attribute you want to use.

From my experience, when you have the public member for ModelExpression, just call it AspFor, if you do not, it does not seem to get any of the attributes. But if the name matches, then I never have an issue. I'm currently using this on .NET 5.

    public class BooleanSelectTagHelper: TagHelper {
                
        public ModelExpression AspFor { get; set; }
        
        public override void Process(TagHelperContext context, TagHelperOutput output){
            //... setup excluded for this example
            var attribute = this
                .AspFor
                .Metadata
                .ContainerType
                .GetProperty(AspFor.Name)
                .GetCustomAttributes(typeof(RequiredAttribute), false)                
                .FirstOrDefault();

            if (attribute is RequiredAttribute requiredAttribute) {
                output.MergeAttribute("data-val", "true");
                output.MergeAttribute("data-val-required", requiredAttribute.FormatErrorMessage(AspFor.Name));                
            }
            //... code to display excluded for this example
        }
    }

I usually attempt to get the attribute, and then check whether it's of the desired type right inside of the if statement. Then access whatever you need to inside of the if branch.

Upvotes: 3

Prolog
Prolog

Reputation: 3374

You are missing HtmlTargetElement attribute on your tag helper:

[HtmlTargetElement(Attributes = "asp-for")] // <== Add this
public class BooleanSelectTagHelper: TagHelper
{
    [HtmlAttributeName("asp-for")]
    public ModelExpression Source { get; set; }
}

Also, don't forget to import your own tag helpers in _ViewImports.cshtml file after Microsoft's tag helpers. Replace the "WebApp" with the name of your assembly containing the tag helpers to add. Read more in official documentation.

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, WebApp

Upvotes: 0

Related Questions