93

I believe this is pretty simple, I just can't seem to find the right way to show the display name for an item within a list within my model.

My simplified model:

public class PersonViewModel
{
    public long ID { get; set; }
    
    private List<PersonNameViewModel> names = new List<PersonNameViewModel>();

    [Display(Name = "Names")]
    public List<PersonNameViewModel> Names { get { return names; } set { names = value; } }      
}

and Names:

public class PersonNameViewModel
{
    public long ID { get; set; }

    [Display(Name = "Set Primary")]
    public bool IsPrimary { get; set; }

    [Display(Name = "Full Name")]
    public string FullName { get; set; }
}

Now I'd like to make a table to show all the names for a person, and get the DisplayNameFor FullName. Obviously,

@Html.DisplayNameFor(model => model.Names.FullName);

wouldn't work, and

@Html.DisplayNameFor(model => model.Names[0].FullName);  

will break if there are no names. Is there a 'best way' to obtain the display name here?

Amal K
  • 4,359
  • 2
  • 22
  • 44
Jonesopolis
  • 25,034
  • 12
  • 68
  • 112
  • Just a note for noobs, if the class attribute is not declared with { get; set; } the DisplayNameFor will not work at all! This rookie mistake happens to pros as well sometimes :D. – Honza P. Dec 22 '21 at 13:35

5 Answers5

149

This actually works, even without items in the list:

@Html.DisplayNameFor(model => model.Names[0].FullName)

It works because MVC parses the expression instead of actually executing it. This lets it find that right property and attribute without needing there to be an element in the list.

It's worth noting that the parameter (model above) doesn't even need to be used. This works, too:

@Html.DisplayNameFor(dummy => Model.Names[0].FullName)

As does this:

@{ Namespace.Of.PersonNameViewModel dummyModel = null; }
@Html.DisplayNameFor(dummyParam => dummyModel.FullName)
Tim S.
  • 55,448
  • 7
  • 96
  • 122
  • I'm assuming that if instead of a `List` you have an `IEnumerable`, then it's also safe to use the expression `model => model.Names.First().FullName`. Is this correct? That said, I think I like the `dummyModel` example best of the three. Otherwise, you could have several properties where you need to type or paste in `model.Names[0].`. Then again, maybe you should refactor the section to a partial view which accepts the `List` or `IEnumerable` as its model. – aaaantoine Mar 24 '16 at 13:12
  • 6
    I tested and `FirstOrDefault()` does work with an empty list. – Josh K Jul 21 '16 at 06:02
  • Unfortunately this doesn't work on IEnumerable property. – Willy David Jr Mar 28 '20 at 01:48
8

There is another way for do it, and i guess that is more clear:

public class Model
{
    [Display(Name = "Some Name for A")]
    public int PropA { get; set; }

    [Display(Name = "Some Name for B")]
    public string PropB { get; set; }
}

public class ModelCollection
{
    public List<Model> Models { get; set; }

    public Model Default
    {
        get { return new Model(); }
    }
}

And then, in the view:

@model ModelCollection

<div class="list">
    @foreach (var m in Model.Models)
    {
        <div class="item">
            @Html.DisplayNameFor(model => model.Default.PropA): @m.PropA
            <br />
            @Html.DisplayNameFor(model => model.Default.PropB): @m.PropB
        </div>
    }
</div>
T-moty
  • 2,679
  • 1
  • 26
  • 31
4

As an alternate solution you could try:

@Html.DisplayNameFor(x => x.GetEnumerator().Current.ItemName)

It will work even if the list is empty!

3

In ASP.NET Core, Html.DisplayNameForInnerType() solves this. It can be used this way:

Html.DisplayNameForInnerType((PersonNameViewModel person) => person.FullName)

Note that the type PersonNameViewModel has to be explicitly specified in the lambda expression's parameter.

This is what the API documentation has to say:

Returns the display name for the specified expression if the current model represents a collection.

It works when the current Model itself is a collection and also when one of its members is a collection.

Amal K
  • 4,359
  • 2
  • 22
  • 44
1

I like T-moty's solution. I needed a solution using generics so my solution is essentially this:

public class CustomList<T> : List<T> where T : new()
{       
    public static async Task<CustomList<T>> CreateAsync(IQueryable<T> source)
    {           
        return new CustomList<T>(List<T> list); //do whatever you need in the contructor, constructor omitted
    }

    private T defaultInstance = new T();

    public T Default
    {
        get { return defaultInstance; }
    }
}

Using this in the view is the same as his example. I create a single instance of an empty object so I'm not creating a new instance every time I reference Default.

Note, the new() constraint is needed in order to call new T(). If your model class doesn't have a default contructor, or you need to add arguments to the constructor you can use this:

private T defaultInstance = (T)Activator.CreateInstance(typeof(T), new object[] { "args" });

The view will have an @model line like:

@model CustomList<Namespace.Of.PersonNameViewModel.Model>
shox
  • 677
  • 1
  • 5
  • 19
  • 1
    Note that `DisplayNameFor` never actually calls the `get` for the property, so it doesn't matter if you create an instance. – NetMage Nov 13 '19 at 00:53