6

I'm trying to display a list of objects in a table. I can iterate over each individual item to find it's value (using an for loop or a DisplayTemplate), but how do I abitriarily pick one to display headers for the whole group.

Here's an simplified example:

Model:

public class ClientViewModel
{
    public int Id { get; set; }
    public List<ClientDetail> Details { get; set; }
}
public class ClientDetail
{
    [Display(Name="Client Number")]
    public int ClientNumber { get; set; }
    [Display(Name = "Client Forname")]
    public string Forname { get; set; }
    [Display(Name = "Client Surname")]
    public string Surname { get; set; }
}

View

@model MyApp.ViewModel.ClientViewModel

@{ var dummyDetail = Model.Details.FirstOrDefault(); }

<table>
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(model => dummyDetail.ClientNumber)</th>
            <th>@Html.DisplayNameFor(model => dummyDetail.Forname)</th>
            <th>@Html.DisplayNameFor(model => dummyDetail.Surname)</th>
        </tr>
    </thead>
    <tbody>
        @for (int i = 0; i < Model.Details.Count; i++)
        {
              <tr>
                <td>@Html.DisplayFor(model => model.Details[i].ClientNumber)</td>
                <td>@Html.DisplayFor(model => model.Details[i].Forname)</td>
                <td>@Html.DisplayFor(model => model.Details[i].Surname)</td>
            </tr>
        }
    </tbody>
</table>

Notice: I'm using var dummyDetail = Model.Details.FirstOrDefault(); to get a single item whose properties I can access in DisplayNameFor.

  • What would be the best way to access those headers ?
  • Will this break if the collection is null?
  • Should I just replace them with hard coded plain text labels?
Community
  • 1
  • 1
akd
  • 6,538
  • 16
  • 70
  • 112

2 Answers2

6

The Problem

As Thomas pointed out, Chris's answer works in some cases, but runs into trouble when using a ViewModel because the nested properties don't enjoy the same automatic resolution. This works if your model type is IEnumerable<Type>, because the DisplayNameFor lambda can still access properties on the model itself:

Model vs Enum<Model>

However, if the ClientDetail collection is nested inside of a ViewModel, we can't get to the item properties from the collection itself:

Collection inside ViewModel

The Solution

As pointed out in DisplayNameFor() From List in Model, your solution is actually perfectly fine. This won't cause any issues if the collection is null because the lambda passed into DisplayNameFor is never actually executed. It's only uses it as an expression tree to identify the type of object.

So any of the following will work just fine:

@Html.DisplayNameFor(model => model.Details[0].ClientNumber)

@Html.DisplayNameFor(dummy => Model.Details.FirstOrDefault().ClientNumber)

@{ ClientDetail dummyModel = null; }
@Html.DisplayNameFor(dummyParam => dummyModel.ClientNumber)

Further Explanation

If we want to see some of the fancy footwork involved in passing an expression, just look at the source code on DisplayNameFor or custom implementations like DescriptionFor. Here's an simplified example of what happens when we call DisplayNameFor with the following impossible expression:

 @Html.DisplayNameFor3(model => model.Details[-5].ClientNumber)

Dissecting the Expression Tree

Notice how we go from model.Details.get_Item(-5).ClientNumber in the lambda expression, to being able to identify just the member (ClientNumber) without executing the expression. From there, we just use reflection to find the DisplayAttribute and get its properties, in this case Name.

Community
  • 1
  • 1
KyleMit
  • 30,350
  • 66
  • 462
  • 664
1

Sorry, your question is a little hard to understand, but I think the gist is that you want to get the display names for your properties, to use as headers, without requiring or first having to pick a particular item out of the list.

There's already built-in support for this. You simply just use the model itself:

@Html.DisplayNameFor(m => m.ClientNumber)

In other words, just don't use a particular instance. DisplayNameFor has logic to inspect the class the list is based on to get the properties.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • yes that what I would like to achieve. but I am aware of DisplayNameFor but you cannot use it to get the attributes inside the collection in the model! If the CLientNumber was part of the Model itself you could do what you say. Table headers are should be populated from a Innumerable ClientSearchVM which is a property inside the Model. so How to dig inside and get the table header from CLientSearchVM class? I know this doesnt work but something like Model.ClientSearchVM.ClientNumber ..etc – akd Jul 14 '14 at 21:59
  • That's what I'm saying. You *can* use it to get a property of the members of the collection. `DisplayNameFor` has internal logic to discern when its passed an expression that evaluates to a collection and inspect the underlying class, instead. – Chris Pratt Jul 14 '14 at 22:13
  • I have attached an image. Coudl you please have a look at that image and tell me How do I get access to ClientNumber? I do not see that option at all. ClientNumber is an attribute of a ClientSearchVM. which is an atttribute inside the model as you can see in the picture. – akd Jul 15 '14 at 10:11
  • 2
    This seems to only be the case if you have `IEnumerable` as your model, what if you have a wrapper around it? – Thomas Boby Feb 18 '16 at 15:35
  • This didn't work for me. Probably because my model is a subclass of PageModel rather than a collection of the thing I'm trying to list. Can't do `@Html.DisplayNameFor(m => m.Users.Name)`. – br3nt Nov 19 '20 at 22:04