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)
serge
  • 13,940
  • 35
  • 121
  • 205

1 Answers1

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;
    });
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • is it possible to update the signature in order to make it work with a collection? like `public static string DisplayShortNameFor( this IHtmlHelper> htmlHelper, Expression> expression) `, because as described in OP the view will not digest the collection – serge Jul 21 '17 at 11:33
  • You don't need one, you can just call it in the view like this: `@Html.ShortNameFor(m => m.First().SomeProperty)` – DavidG Jul 21 '17 at 12:04
  • 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 – serge Jul 21 '17 at 12:06
  • No you won't get any errors, try it out. It's based on expressions not an executed `Func`. – DavidG Jul 21 '17 at 12:07
  • ok, just because the signature I proposed with collection is actually in the DispayNameFor MS Core code – serge Jul 21 '17 at 12:08
  • I tried to modify your MetaDataFor in `private static IHtmlContent MetaDataFor(this IHtmlHelper> html, Expression> expression, Func property)` but does not compile because of `FromLambdaExpression` – serge Jul 21 '17 at 12:10
  • 1
    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. – DavidG Jul 21 '17 at 12:53
  • The problem I discovered with this solution is that it does not support the localization... if I have the `Models.MyModel.en.resx` it doesn't care about... – serge Jul 24 '17 at 17:26
  • 1
    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);` – Phil Mar 18 '20 at 04:59
  • @Phil Tak a look at https://stackoverflow.com/q/59000215/3074765 . – Jim Wilcox Apr 01 '20 at 12:37
  • Is there a way to set Name property of the Display attribute to automatically the proper itself? `[Display]` `public string Email { get; set; }` So location key is automatically "Email" ? – Nathan Mar 01 '21 at 22:32