74

I have a model class, with a property like this:

[Display(Name = "Phone", Description="Hello World!")]
public string Phone1 { get; set; }

Displaying a label and rendering a textbox for input in my view is pretty easy:

@Html.LabelFor(model => model.Organization.Phone1)
@Html.EditorFor(model => model.Organization.Phone1)
@Html.ValidationMessageFor(model => model.Organization.Phone1)

But how do I render the value of the Description annotation attribute, i.e. "Hello World!"??

huysentruitw
  • 27,376
  • 9
  • 90
  • 133
Jakob Gade
  • 12,319
  • 15
  • 70
  • 118
  • See also [ShortName in the Display attribute (DataAnnotations)](https://stackoverflow.com/a/39960755/1366033) – KyleMit Mar 14 '18 at 15:36

13 Answers13

87

I ended up with a helper like this:

using System;
using System.Linq.Expressions;
using System.Web.Mvc;

public static class MvcHtmlHelpers
{
    public static MvcHtmlString DescriptionFor<TModel, TValue>(this HtmlHelper<TModel> self, Expression<Func<TModel, TValue>> expression)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
        var description = metadata.Description;

        return MvcHtmlString.Create(string.Format(@"<span>{0}</span>", description));
    }
}

Thanks to those who led me in the right direction. :)

Jakob Gade
  • 12,319
  • 15
  • 70
  • 118
  • 5
    to eliminate the extra span, I changed the return to `return string.IsNullOrWhiteSpace(description) ? MvcHtmlString.Empty : MvcHtmlString.Create(string.Format(@"{0}", description));` – ScottCate Jan 06 '13 at 18:51
  • 4
    Starting with your example, I build this out a little more, with the ability to override the span tag, and optionally add a cssClass. https://gist.github.com/4469809 – ScottCate Jan 06 '13 at 20:03
  • 1
    @ScottCate, instead of taking a `cssClass` parameter, you could take a `htmlAttributes` parameter instead as is done in `EditorFor`. – Fred Jun 23 '15 at 13:00
  • If you need to support a `DescriptionAttribute` instead of `DisplayAttribute`, [see my additional information](http://stackoverflow.com/a/35343858/2441442). – Christian Gollhardt Feb 11 '16 at 15:58
  • 3
    I wonder why Description, and for that matter, Prompt exist as properties of that attribute if WE have to write extensions just to spit 'em out? – Scott Fraley Dec 19 '16 at 22:55
  • I'd suggest the MvcHtmlString is returned with just the description content, and allow the implementing razor view to decide which tag it is wrapped in. – Aaron Hudon Jun 27 '17 at 14:35
  • For some unknown reason my instance of VS decided to use Antlr.Func instead of System.Func, so thought I'd give a warning to anyone else who might have the red squiggles for no apparent reason. – Setherith Dec 04 '18 at 10:49
41

Using the technique from this article about how to Display visual hints for the fields in your form, you can access the value via the following:

@Html.TextBoxFor( 
        model => model.Email , 
        new { title = ModelMetadata.FromLambdaExpression<RegisterModel , string>( 
            model => model.Email , ViewData ).Description } )  
KyleMit
  • 30,350
  • 66
  • 462
  • 664
Adam Tuliper
  • 29,982
  • 4
  • 53
  • 71
  • 4
    one could even take this a step further and update the T4 templates so that new CRUD views were scaffolded using this method – Dylan Hayes Nov 05 '13 at 03:30
27

In ASP.NET MVC Core you can use the new Tag Helpers, that makes your HTML look like... HTML :)

Like this:

<div class="form-group row">
    <label asp-for="Name" class="col-md-2 form-control-label"></label>
    <div class="col-md-10">
        <input asp-for="Name" class="form-control" aria-describedby="Name-description" />
        <span asp-description-for="Name" class="form-text text-muted" />
        <span asp-validation-for="Name" class="text-danger" />
    </div>
</div>

Note 1: You can use the aria-describedby attribute in the input element as that id will be created automatically in the span element with asp-description-for attribute.

Note 2: In Bootstrap 4, the classes form-text and text-muted replaces the v3 help-block class for block-level help text.

For this magic to happen, you just need to create a new Tag Helper:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;span&gt; elements with an <c>asp-description-for</c> attribute.
/// Adds an <c>id</c> attribute and sets the content of the &lt;span&gt; with the Description property from the model data annotation DisplayAttribute.
/// </summary>
[HtmlTargetElement("span", Attributes = DescriptionForAttributeName)]
public class SpanDescriptionTagHelper : TagHelper
{
    private const string DescriptionForAttributeName = "asp-description-for";

    /// <summary>
    /// Creates a new <see cref="SpanDescriptionTagHelper"/>.
    /// </summary>
    /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param>
    public SpanDescriptionTagHelper(IHtmlGenerator generator)
    {
        Generator = generator;
    }

    /// <inheritdoc />
    public override int Order
    {
        get
        {
            return -1000;
        }
    }

    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    protected IHtmlGenerator Generator { get; }

    /// <summary>
    /// An expression to be evaluated against the current model.
    /// </summary>
    [HtmlAttributeName(DescriptionForAttributeName)]
    public ModelExpression DescriptionFor { get; set; }

    /// <inheritdoc />
    /// <remarks>Does nothing if <see cref="DescriptionFor"/> is <c>null</c>.</remarks>
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (output == null)
        {
            throw new ArgumentNullException(nameof(output));
        }

        var metadata = DescriptionFor.Metadata;

        if (metadata == null)
        {
            throw new InvalidOperationException(string.Format("No provided metadata ({0})", DescriptionForAttributeName));
        }

        output.Attributes.SetAttribute("id", metadata.PropertyName + "-description");

        if( !string.IsNullOrWhiteSpace( metadata.Description))
        {
            output.Content.SetContent(metadata.Description);
            output.TagMode = TagMode.StartTagAndEndTag;
        }
    }
}

And make your Tag Helpers available to all our Razor views. Add the addTagHelper directive to the Views/_ViewImports.cshtml file:

@addTagHelper "*, YourAssemblyName"

Note 1: Replace YourAssemblyName with the assembly name of your project.

Note 2: You just need to do this once, for all your Tag Helpers!

More information on Tag Helpers here: https://docs.asp.net/en/latest/mvc/views/tag-helpers/intro.html

That’s it! Have fun with the new Tag Helpers!

Filipe Carneiro
  • 448
  • 5
  • 5
  • 11
    Hi Stephen! That's why I've started with "In ASP.NET MVC Core...". The question is exactly the same for both worlds and, although this is an old question, I believe people, like me, are stumbling in this question for answers today! I've looked here for answers earlier today and, after several hours of research, I've decided to share my conclusions for our pears. Hope someone find that useful! – Filipe Carneiro Sep 23 '16 at 00:22
  • 2
    The best solution to me. Since HtmlHelpers can be considered a way too old and obsolete TagHelpers are the preferred way to handle this. This answer deserves many more upvotes! – Alexander Christov Apr 21 '20 at 07:56
24

I was about to use the accepted answer, but it didn't work for ASP.NET Core 1/2 (a.k.a. MVC 6) because ModelMetadata.FromLambdaExpression no longer exists and has been moved to ExpressionMetadataProvider (also usage has been changed slightly).

This is an updated extension method you can use with ASP.NET Core 1.1 & 2:

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

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)}");

        return new HtmlString(modelExplorer.Metadata.Description);
    }
}

ASP.NET Core 1

For ASP.NET Core 1, the same code works, but you'll need different namespace usings:

using System;
using System.Linq.Expressions;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.ViewFeatures;

Usage

@Html.DescriptionFor(model => model.Phone1)
huysentruitw
  • 27,376
  • 9
  • 90
  • 133
  • So, the namespaces for my ASP.NET Core 1.1 to use are the following: `Microsoft.AspNetCore.Html` `Microsoft.AspNetCore.Mvc.ViewFeatures.Internal` `Microsoft.AspNetCore.Mvc.Rendering` – serge Jul 21 '17 at 08:10
  • How to make the solution for for the "ShortName" property? There is no such thing as `modelExplorer.Metadata.ShortName` – serge Jul 21 '17 at 08:29
  • @Html.DescriptionFor(model => model.First().Phone1) in case of IEnumerable – user1855805 Jul 18 '18 at 05:11
  • Anyone have difficulty with the Microsoft.AspNetCore.Mvc.ViewFeatures.Internal namespace? This one is showing in red despite installing the 'Microsoft.AspNetCore.Mvc.ViewFeatures' package. I'm on .NET Core 3.0 – Ciaran Gallagher Jan 25 '20 at 18:46
  • 1
    It turns out this library is not compatible with ASP.NET Core 3.0 any longer, it has to be retrieved from a different type via dependency injection: https://github.com/dotnet/aspnetcore/issues/12643#issuecomment-515702458 – Ciaran Gallagher Jan 25 '20 at 18:51
  • I've updated this for ASP.NET Core 3 and 5: https://stackoverflow.com/a/66602099/2266345 – Ryan Mar 12 '21 at 14:47
6

If anyone is wondering how to use the accepted answer

1- In your solution explorer > Add new folder > name it"Helpers" for example
2- Add a new class, name it "CustomHtmlHelpers" for example
3- Paste the code :

public static class MvcHtmlHelpers
{
    public static string DescriptionFor<TModel, TValue>(this HtmlHelper<TModel> self, Expression<Func<TModel, TValue>> expression)
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
        var description = metadata.Description;

        return string.IsNullOrWhiteSpace(description) ? "" : description;
    }
}

4- In your model or viewModel using it this:

[Display(Name = "User Name", Description = "Enter your User Name")]
public string FullName { get; set; }

5- In your Razor view, after the @model, type this line

@using YOUR_PROJECT.Helpers 

6- Display the description like this:

@Html.DescriptionFor(m => m.FullName) 

7- You may want to use the description to display text in the input placeholder:

@Html.DisplayNameFor(m => m.FullName)
@Html.TextBoxFor(m => m.FullName, new { @class = "form-control", placeholder = Html.DescriptionFor(m => m.FullName) })

Thanks

Adel Mourad
  • 1,351
  • 16
  • 13
4
var attrib = (DisplayAttribute)Attribute.GetCustomAttribute(
             member, typeof(DisplayAttribute));
var desc = attrib == null ? "" : attrib.GetDescription()
vvvv4d
  • 3,881
  • 1
  • 14
  • 18
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Works. I needed to use DescriptionAttribute instead of DisplayAttribute.. – vvvv4d Jun 04 '19 at 23:45
  • 2
    @vvvv4d then... do that? just use `DescriptionAttribute` instead, and access `.Description` for the final value, or `.DescriptionValue` for the raw string – Marc Gravell Jun 05 '19 at 07:52
4

Here is an updated version for ASP.NET Core 3.1 and 5:

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 expressionProvider = html.ViewContext?.HttpContext?.RequestServices?.GetService<ModelExpressionProvider>()
            ?? new ModelExpressionProvider(html.MetadataProvider);
        var modelExpression = expressionProvider.CreateModelExpression(html.ViewData, expression);

        return new HtmlString(modelExpression.Metadata.Description);
    }
}

We have to go via ModelExpressionProvider now that ExpressionMetadataProvider is marked internal.

ModelExpressionProvider.CreateModelExpression() calls ExpressionMetadataProvider.FromLambdaExpression() internally anyway:

https://github.com/aspnet/Mvc/blob/04ce6cae44fb0cb11470c21769d41e3f8088e8aa/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ModelExpressionProvider.cs#L42

Ryan
  • 1,670
  • 18
  • 25
2
@ViewData.ModelMetadata.Properties
   .Where(m => m.PropertyName == "Phone1").FirstOrDefault().Description

So, if you were using bootstrap, something like

<div class="form-group col-sm-6">
   @Html.LabelFor(m => m.Organization.Phone1)
   @Html.EditorFor(m => m.Organization.Phone1)
   <p class="help-block">
      @ViewData.ModelMetadata.Properties
         .Where(m => m.PropertyName == "DayCount").FirstOrDefault().Description
   </p>
</div>
MikeT
  • 2,530
  • 25
  • 36
  • @ViewData.ModelMetadata.Properties.FirstOrDefault(m => m.PropertyName == nameof(ServiceRequestPageViewModel.AcceptTerms))?.DisplayName just an update with null check and nameof to avoid runtime errors – EthR Sep 28 '18 at 16:37
1

...and if you prefer to have the description as a tooltip in the form label, add a Tag Helper like this:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;label&gt; elements with an <c>asp-for</c> attribute.
/// Adds a <c>title</c> attribute to the &lt;label&gt; with the Description property from the model data annotation DisplayAttribute.
/// </summary>
[HtmlTargetElement("label", Attributes = ForAttributeName)]
public class LabelTitleTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-for";

    /// <summary>
    /// Creates a new <see cref="LabelTitleTagHelper"/>.
    /// </summary>
    /// <param name="generator">The <see cref="IHtmlGenerator"/>.</param>
    public LabelTitleTagHelper(IHtmlGenerator generator)
    {
        Generator = generator;
    }

    /// <inheritdoc />
    public override int Order
    {
        get
        {
            return -1000;
        }
    }

    [HtmlAttributeNotBound]
    [ViewContext]
    public ViewContext ViewContext { get; set; }

    protected IHtmlGenerator Generator { get; }

    /// <summary>
    /// An expression to be evaluated against the current model.
    /// </summary>
    [HtmlAttributeName(ForAttributeName)]
    public ModelExpression TitleFor { get; set; }

    /// <inheritdoc />
    /// <remarks>Does nothing if <see cref="TitleFor"/> is <c>null</c>.</remarks>
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (output == null)
        {
            throw new ArgumentNullException(nameof(output));
        }

        var metadata = TitleFor.Metadata;

        if (metadata == null)
        {
            throw new InvalidOperationException(string.Format("No provided metadata ({0})", ForAttributeName));
        }

        if (!string.IsNullOrWhiteSpace(metadata.Description))
            output.Attributes.SetAttribute("title", metadata.Description);
    }
}

That will create a new title attribute with the Description property from the model's data annotation DisplayAttribute.

The beautiful part is that you don't need to touch your generated scaffolded views! Because this Tag Helper is targeting the asp-for attribute of the label element that is already there!

Filipe Carneiro
  • 448
  • 5
  • 5
  • I love how this makes use of the existing asp-for, saves me having to modify every label across my website. Nice! – Mike Jul 02 '20 at 03:27
1

You would have to write a custom helper that would reflect on your model to give the Description attribute value.

Illuminati
  • 4,539
  • 2
  • 35
  • 55
  • 1
    I was hoping there would be something built into the MVC framework, especially since Dynamic Data supports the attribute, rendering the text as a discrete tooltip. – Jakob Gade Jul 05 '11 at 06:09
  • 1
    As far as i know there isnt anything like taht yet built into. have look at this... http://stackoverflow.com/questions/4973830/extract-display-name-and-description-attribute-from-within-a-html-helper – Illuminati Jul 05 '11 at 06:11
0

In addition to Jakob Gade'a great answer:

If you need to Support a DescriptionAttribute instead of a DisplayAttribute, his great solution still works if we override the MetadataProvider:

public class ExtendedModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<System.Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        //Possible Multiple Enumerations on IEnumerable fix
        var attributeList = attributes as IList<System.Attribute> ?? attributes.ToList();

        //Default behavior
        var data = base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);

        //Bind DescriptionAttribute
        var description = attributeList.SingleOrDefault(a => typeof(DescriptionAttribute) == a.GetType());
        if (description != null)
        {
            data.Description = ((DescriptionAttribute)description).Description;
        }

        return data;
    }
}

This need to be registeres in the Application_Start Method in Global.asax.cs:

ModelMetadataProviders.Current = new ExtendedModelMetadataProvider();
Community
  • 1
  • 1
Christian Gollhardt
  • 16,510
  • 17
  • 74
  • 111
-1

HANDL's answer, updated for ASP.NET Core 2.0

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

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)}");

        return new HtmlString(modelExplorer.Metadata.Description);
    }
}
Randy Gamage
  • 1,801
  • 6
  • 22
  • 31
  • Already covered in [this answer](https://stackoverflow.com/a/35294035/1300910). Namespace updates should have been done in the original answer or by adding a comment instead of posting a duplicate. – huysentruitw Feb 02 '18 at 14:21
-2

You can always create your own custom extension like this:

    public static MvcHtmlString ToolTipLabel (string resourceKey, string text, bool isRequired, string labelFor = "", string labelId = "",string className="")
    {
        string tooltip = string.Empty;

        StringBuilder sb = new StringBuilder();

        if (!string.IsNullOrEmpty(resourceKey))
        {
            var resources = GetAllResourceValues();

            if (resources.ContainsKey(resourceKey))
            {
                tooltip = resources[resourceKey].Value;
            }
        }

        sb.Append("<label");

        if (!string.IsNullOrEmpty(labelFor))
        {
            sb.AppendFormat(" for=\"{0}\"", labelFor);
        }

        if (!string.IsNullOrEmpty(labelId))
        {
            sb.AppendFormat(" Id=\"{0}\"", labelId);
        }

        if (!string.IsNullOrEmpty(className))
        {
            sb.AppendFormat(" class=\"{0}\"", className);
        }

        if (!string.IsNullOrEmpty(tooltip))
        {

            sb.AppendFormat(" data-toggle='tooltip' data-placement='auto left' title=\"{0}\"",tooltip);

        }
        if (isRequired)
        {
            sb.AppendFormat("><em class='required'>*</em> {0} </label></br>", text);
        }
        else
        {
            sb.AppendFormat(">{0}</label></br>", text);
        }
        return MvcHtmlString.Create(sb.ToString());
    }

and can get it in view like this:

@HtmlExtension.ToolTipLabel(" "," ",true," "," "," ")
Karan
  • 53
  • 3
  • The question is how to get the description from the display-attribute in the view, I don't see how this piece of code is going to help. – huysentruitw Feb 12 '18 at 08:15