0

I got a model called Foo that is a Bar property.

// Foo model
public class Foo
{
    public string Bar { get; set; } = "";
}

Then I created a details view

@model Models.Foo

<h1>Details @Html.DisplayNameFor(model => model)</h1>

<dl class="row">
    <dt class = "col-sm-2">
        @Html.DisplayNameFor(model => model.Bar)
    </dt>
    <dd class = "col-sm-10">
        @Html.DisplayFor(model => model.Bar)
    </dd>
</dl>

By default DisplayNameFor is rendering the name of the property thus, I was expecting the same behavior for the model's name but instead, I have got an empty string. The result is the same even when I use the DisplayNameForModel

...
<h1>Details @Html.DisplayNameForModel()</h1>
...

I can add a display name to fix that but I don't want to open that door yet. I'm postponing internationalization further to avoid mixing my models with UI stuff.

// Foo model
[DisplayName("Foo")] // I'm trying to avoid this solution
public class Foo
{
    public string Bar { get; set; } = "";
}

It's even awkward because it works (without setting DisplayName) when Foo model is a property of another class.

Am I missing something or currently there isn't possible to archive that? Can I open a request to the project's maintainers to fix that? Where?

Thanks!

jbatista
  • 964
  • 2
  • 11
  • 26

1 Answers1

1

Referencing the documentation for DisplayNameFor, the description for the expression parameter says: "An expression to be evaluated against the current model." It's possible that the implementation is evaluated against an item in the current model and not against the model class itself. This fits the pattern where you get the name of Foo when it's a property in another class (i.e. you're getting the name of the property named Foo versus the class name).

Issue: Inconsistent fallback when DisplayName attribute is missing

There's an inconsistency in the expected output of the built-in DisplayNameFor HTML Helper in the Microsoft.AspNetCore.Mvc.ViewFeatures namespace. (https://source.dot.net/#Microsoft.AspNetCore.Mvc.ViewFeatures/HtmlHelperOfT.cs,93a34f38f20458b3)

When a DisplayName attribute is provided for a class definition and when a DisplayName attribute is provided for a property class member within the class body, the DisplayNameFor HTML Helper returns the value for the DisplayName attribute in both cases.

[DisplayName("Foo Class")]
public class Foo
{
    public string Id { get; set; }

    [DisplayName("Bar Property")]
    public string Bar { get; set; }
}

When a DisplayName attribute is not provided for the class definition or a property class member, the DisplayNameFor HTML Helper returns an empty string for the class definition but returns the name of the property within the class body. The expected result for the class definition is to fallback to the name of the class definition as DisplayNameFor does with the name of the property within the class body.

public class Foo // HTML Helper returns empty string
{
    public string Id { get; set; }

    public string Bar { get; set; } // Fallback to 'Bar' in HTML Helper
}

Using Reflection

An alternate method to display the name of the model in a Razor page, the GetType() method from Reflection works.

@Model.GetType().Name

The Name property gets the same label you're expecting from @Html.DisplayNameFor(model => model).

Adding @using System.Reflection to the top of the view page may be necessary if the GetType() does not resolve correctly.

Adding Custom HTML Helper

(2/27/2023 Code addition to address @jbatista's comment)

Add a custom tag helper as this Stack Overflow answer outlines.

Use GetCustomAttribute to get the value of the display name attribute. This Microsoft documentation page provides a good example.

Pulling code from the two links referenced above, here's a custom HTML Helper that gets the value of the [DisplayName("name")] attribute for a model in a Razor page.

MyHTMLHelpers.cs

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel;

namespace WebApplication1.Helpers
{
    // https://stackoverflow.com/questions/42039269/create-custom-html-helper-in-asp-net-core
    public static class MyHTMLHelpers
    {
        public static IHtmlContent DisplayNameForModel2(
            this IHtmlHelper htmlHelper, object model)
        {
            // Retrieving a Single Instance of an Attribute
            // https://learn.microsoft.com/en-us/dotnet/standard/attributes/retrieving-information-stored-in-attributes#retrieving-a-single-instance-of-an-attribute

            // Get instance of the 'DisplayName' attribute
            DisplayNameAttribute? displayNameAttr =
                (DisplayNameAttribute?)Attribute.GetCustomAttribute(
                    model.GetType(), typeof(DisplayNameAttribute));

            string displayName = displayNameAttr == null ?
                  model.GetType().Name :
                  displayNameAttr.DisplayName.ToString();

            return new HtmlString($"<strong>{displayName}</strong>");
        }
    }
}

namespace WebApplication1.Data
{
    [DisplayName("Foo bar")]
    public class Foo
    {
        public string Id { get; set; }
        public string Description { get; set; }
    }
}

Razor page (DisplayNameForModelHtmlHelper.cshtml) - The @using and @addTagHelper statements can be added to a _ViewImports.cshtml file per the Stack Overflow answer.

@page
@using WebApplication1
@using WebApplication1.Helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model WebApplication1.Pages.DisplayNameForModelHtmlHelperModel
@{
}
<div>@Html.DisplayNameForModel2(Model.Foo)</div>
@Model.Foo.Description

DisplayNameForModelHtmlHelper.cshtml.cs

using Microsoft.AspNetCore.Mvc.RazorPages;
using WebApplication1.Data;

namespace WebApplication1.Pages
{
    public class DisplayNameForModelHtmlHelperModel : PageModel
    {
        public Foo Foo { get; set; } = new Foo()
        {
            Id = "An ID",
            Description = "Description of foo"
        };

        public void OnGet()
        {
        }
    }
}

Rendered HTML

Foo bar
Description of foo

Your original code that didn't output the expected result:

<h1>Details @Html.DisplayNameForModel()</h1>

can be replaced with a custom HTML Helper to get the expected result:

<h1>Details @Html.DisplayNameForModel2(Model)</h1>

Accepted answer

(@jbatista base on @dave previous answer section)

I create a DisplayNameFor extension method instead of DisplayNameForModel2 to have a consistent DSL since it's the method name used for the model's attributes.

public static class AspNetCoreMvcRenderingMissingExtentionMethods
{
    public static string DisplayNameFor(this IHtmlHelper htmlHelper, object model)
    {
        DisplayNameAttribute? displayNameAttr = (DisplayNameAttribute?)Attribute.GetCustomAttribute(model.GetType(), typeof(DisplayNameAttribute));
        return displayNameAttr == null ? model.GetType().Name : displayNameAttr.DisplayName.ToString();
    }
}

Then I can call it like that:

<h1>Create @Html.DisplayNameFor(Model)</h1>

The next step is to isolate this into its own project (Microsoft.AspNetCore.Mvc.Rendering.MissingMethods?) while figuring out if makes sense to report that to the project maintainers - shouldn't we get something like that by default?

Dave B
  • 1,105
  • 11
  • 17
  • Great answer @Dave. However, I don't think that the `GetType` approach is a valid solution here because misses the `DisplayName` attribute decoration. I still think that it should be possible to display name for model itself. – jbatista Feb 27 '23 at 23:09
  • 1
    I agree that `@Html.DisplayNameForModel()` should produce the same output as it did. I added a custom HTML Helper to the answer above to get the `DisplayName` attribute. If a `DisplayName` attribute exists, then its value is used. Otherwise, `model.GetType().Name` is returned from the custom helper. – Dave B Feb 28 '23 at 02:15
  • I accepted your suggestion, but I added `DisplayNameFor` instead of `DisplayNameForModel2` to have a consistent DSL since it's the method name used for the model's attributes. I give you the bounty but I still would like to report it to the maintainers to be included by default. Can you help me with that? Did you know here can I open the issue? – jbatista Feb 28 '23 at 17:53
  • 1
    Using `DisplayNameFor` works as the name of the custom HTML Helper because there's a different parameter definition for the custom versus the built-in helper so your addition is good. I cleaned up the answer and added content that summarizes the issue from your original post. ASP.Net Core issues/bugs can be raised on their GitHub repo: https://github.com/dotnet/aspnetcore/issues. The .NET support home page is https://dotnet.microsoft.com/en-us/platform/support. – Dave B Feb 28 '23 at 21:53