1

In an ASP .NET Core 1.1 project (VS 2017) I try to use the ShortName attrubute of the Display property in order to use the DisplayFor HTML Helper:

[Display(Name="Project Name", ShortName="Name", Description="The name of the project")]
public string Name { get; set; }

I read the following answer that does the trick for the Description. Unfortunately for a reason I don't understand, this doesn't work for the ShortName.

There is the code I tried, the first method seems OK, but the second does not compile, so I would like to fix it:

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using System;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;

namespace MyProject.Helpers
{
    public static class HtmlExtensions
    {
        public static IHtmlContent DescriptionFor<TModel, TValue>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
        {
            if (html == null) throw new ArgumentNullException(nameof(html));
            if (expression == null) throw new ArgumentNullException(nameof(expression));

            var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, html.MetadataProvider);
            if (modelExplorer == null) throw new InvalidOperationException($"Failed to get model explorer for {ExpressionHelper.GetExpressionText(expression)}");
            //////// Description is OK 
            return new HtmlString(modelExplorer.Metadata.Description);
        }

        public static IHtmlContent ShortNameFor<TModel, TValue>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
        {
            if (html == null) throw new ArgumentNullException(nameof(html));
            if (expression == null) throw new ArgumentNullException(nameof(expression));

            var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, html., html.MetadataProvider);
            if (modelExplorer == null) throw new InvalidOperationException($"Failed to get model explorer for {ExpressionHelper.GetExpressionText(expression)}");
            //////// ShortName DOES NOT EXIST !!!!!!!!!!!!!!!!
            return new HtmlString(modelExplorer.Metadata.ShortName);
        }
    }
}

More that than, reviewing the MS code of the DisplayNameFor

the signature of the method should change for something like this:

public static string DisplayShortNameFor<TModelItem, TResult>(
    this IHtmlHelper<IEnumerable<TModelItem>> htmlHelper,
    Expression<Func<TModelItem, TResult>> expression)    

and not

public static IHtmlContent ShortNameFor<TModel, TValue>(
    this IHtmlHelper<TModel> html, 
    Expression<Func<TModel, TValue>> expression)

Update

For the old signature I tried

public static string DisplayShortNameFor<TModel, TValue>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
    string shortNameValue = string.Empty;
    var prop = expression.Body as MemberExpression;
    if (prop != null)
    {
        var DisplayAttrib = prop.Member.GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault();
        if (DisplayAttrib != null)
            shortNameValue = DisplayAttrib.ShortName;
    }
    return shortNameValue;
}

but actually I can't run it because does not compile in the View, because is a IEnumerable

@using MyProject.Helpers
@model IEnumerable<MyProject.Models.Record> <!--<<< IEnumerable to display a collection -->

@Html.DisplayShortNameFor(model => model.Name)

So I need to do

// for my method shortname I need to use FirstOfDefault...
@Html.DisplayShortNameFor(model => model.FirstOrDefault().Name)

// but for ASP.NET DisplayName works
@Html.DisplayNameFor(model => model.Date)
0

1 Answer 1

4

To get the ShortName property using this method, you need to extract the Display attribute manually because it's not part of the default metadata. For example, something like this will work:

var defaultMetadata = m as 
    Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
if(defaultMetadata != null)
{
    var displayAttribute = defaultMetadata.Attributes.Attributes
        .OfType<DisplayAttribute>()
        .FirstOrDefault();
    if(displayAttribute != null)
    {
        return displayAttribute.ShortName;
    }
}
return m.DisplayName;

To plug that into your helpers, I would abstract away the method slightly as there's some duplicate code in there, so you would end up with a private method like this:

private static IHtmlContent MetaDataFor<TModel, TValue>(this IHtmlHelper<TModel> html, 
    Expression<Func<TModel, TValue>> expression,
    Func<ModelMetadata, string> property)
{
    if (html == null) throw new ArgumentNullException(nameof(html));
    if (expression == null) throw new ArgumentNullException(nameof(expression));

    var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, html.MetadataProvider);
    if (modelExplorer == null) throw new InvalidOperationException($"Failed to get model explorer for {ExpressionHelper.GetExpressionText(expression)}");
    return new HtmlString(property(modelExplorer.Metadata));
}

And your two public methods like this:

public static IHtmlContent DescriptionFor<TModel, TValue>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
    return html.MetaDataFor(expression, m => m.Description);
}

public static IHtmlContent ShortNameFor<TModel, TValue>(this IHtmlHelper<TModel> html, 
    Expression<Func<TModel, TValue>> expression)
{
    return html.MetaDataFor(expression, m => 
    {
        var defaultMetadata = m as 
            Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata;
        if(defaultMetadata != null)
        {
            var displayAttribute = defaultMetadata.Attributes.Attributes
                .OfType<DisplayAttribute>()
                .FirstOrDefault();
            if(displayAttribute != null)
            {
                return displayAttribute.ShortName;
            }
        }
        //Return a default value if the property doesn't have a DisplayAttribute
        return m.DisplayName;
    });
}
Sign up to request clarification or add additional context in comments.

11 Comments

is it possible to update the signature in order to make it work with a collection? like public static string DisplayShortNameFor<TModelItem, TResult>( this IHtmlHelper<IEnumerable<TModelItem>> htmlHelper, Expression<Func<TModelItem, TResult>> expression) , because as described in OP the view will not digest the collection
You don't need one, you can just call it in the view like this: @Html.ShortNameFor(m => m.First().SomeProperty)
yes, but where there are no items I will get errors... and this a one more call t the collection, I mean, actual ASP.NET Code is with Collection inside, not a member
Well it's a lot more complicated to do it for the IEnumerable case, I'll leave that as an exercise for you because the option above works perfectly well.
With Core 3.0 the ExpressionMetadataProvider is no longer available as a static class (it's there but internal only) instead the following can be used: var expressionMetadataHelper = (ModelExpressionProvider)html.ViewContext.HttpContext.RequestServices.GetService(typeof(ModelExpressionProvider)); var modelExplorer = expressionMetadataHelper.CreateModelExpression(html.ViewData, expression);
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.