48

I have the following model in MVC:

public class ParentModel
{
    public string Property1 { get; set; }
    public string Property2 { get; set; }

    public IEnumerable<ChildModel> Children { get; set; }
}

When I want to display all of the children for the parent model I can do:

@Html.DisplayFor(m => m.Children)

I can then create a ChildModel.cshtml display template and the DisplayFor will automatically iterate over the list.

What if I want to create a custom template for IEnumerable?

@model IEnumerable<ChildModel>

<table>
    <tr>
        <th>Property 1</th>
        <th>Property 2</th>
    </tr>
    ...
</table>

How can I create a Display Template that has a model type of IEnumerable<ChildModel> and then call @Html.DisplayFor(m => m.Children) without it complaining about the model type being wrong?

Farhad Jabiyev
  • 26,014
  • 8
  • 72
  • 98
Dismissile
  • 32,564
  • 38
  • 174
  • 263

4 Answers4

69

Like this:

@Html.DisplayFor(m => m.Children, "YourTemplateName")

or like this:

[UIHint("YourTemplateName")]
public IEnumerable<ChildModel> Children { get; set; }

where obviously you would have ~/Views/Shared/DisplayTemplates/YourTemplateName.cshtml:

@model IEnumerable<ChildModel>

<table>
    <tr>
        <th>Property 1</th>
        <th>Property 2</th>
    </tr>
    ...
</table>
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Is there really no way to declare a template that would auto hook up as the default for any IEnumerable? – Maslow Feb 06 '13 at 15:29
  • You could use the `object.cshtml` template for that. – Darin Dimitrov Feb 06 '13 at 15:57
  • can you hook up to `object.cshtml` in such a way that the default behavior is maintained except for my one special type? – Maslow Feb 06 '13 at 17:05
  • 5
    I don't believe this answer is 100% correct.. 1) If DisplayFor is called with the template name, it will not automatically iterate over collection; 2) Specifying @model ICollection in display template will not allow accessing ChildModel properties without retrieving an item from collection i.e. foreach needs to be used – Jack0fshad0ws Sep 08 '13 at 22:30
  • 4
    @Jack0fshad0ws, you are absolutely right. And that's the reason why in my answer I used `@model IEnumerable` in the template. Because when you specify a template name, the model that is passed to the template is the collection property. – Darin Dimitrov Sep 09 '13 at 06:01
  • Not sure what advantage this provides over @Html.Partial, though. – Josh Kodroff Jan 29 '14 at 19:08
  • @JoshKodroff, in this particular case it doesn't provide any advantage over Html.Partial. The real power of Editor/Display templates come when you rely on the standard conventions instead of using `UIHint`. In this case editor templates will preserve the navigational context and generate proper names of the input fields. Also they will avoid you the need of writing ugly `foreach` loops in your views as they will automatically render the template for each element of collection properties. – Darin Dimitrov Jan 29 '14 at 21:51
  • 3
    Am I understanding correctly that there is no way to (a) avoid foreach AND (b) specify the name of a template? In the example above, I would still need to @foreach through the table rows. – Marc Stober Feb 12 '14 at 16:42
  • As far as I can tell @MarcStober is correct. I've not been able to find a way to do this without a foreach loop if I want to specify a particular template. – NightWatchman Jun 02 '16 at 21:02
  • 1
    These comments are absolutely incorrect. An MVC template will automatically enumerate over a collection and recreate it's contents once for each item. All you have to do is pass a model of IEnumerable and name the display template (partial) exactly as you have your model class named.. with the exception of the leading underscore. – kennythecoder Mar 15 '17 at 18:03
  • 2
    Correction... you pass a collection of IEnumerable to your template but the model for your template will be singular. MVC with re-create the template once for each item in your collection. – kennythecoder Mar 15 '17 at 19:51
  • 1
    Old comment but Kenny is right. If your display template is for FooViewModel and your view uses IEnumerable. Calling DisplayForModel() will display the template for each object in the collection. – perustaja Dec 24 '19 at 00:44
8

This is in reply to Maslow's comment. This is my first ever contribution to SO, so I don't have enough reputation to comment - hence the reply as an answer.

You can set the 'TemplateHint' property in the ModelMetadataProvider. This would auto hookup any IEnumerable to a template you specify. I just tried it in my project. Code below -

protected override CachedDataAnnotationsModelMetadata CreateMetadataFromPrototype(CachedDataAnnotationsModelMetadata prototype, Func<object> modelAccessor)
    {
        var metaData = base.CreateMetadataFromPrototype(prototype, modelAccessor);
        var type = metaData.ModelType;

        if (type.IsEnum)
        {
            metaData.TemplateHint = "Enum";
        }
        else if (type.IsAssignableFrom(typeof(IEnumerable<object>)))
        {
            metaData.TemplateHint = "Collection";
        }

        return metaData;
    }

You basically override the 'CreateMetadataFromPrototype' method of the 'CachedDataAnnotationsModelMetadataProvider' and register your derived type as the preferred ModelMetadataProvider.

In your template, you cannot directly access the ModelMetadata of the elements in your collection. I used the following code to access the ModelMetadata for the elements in my collection -

@model IEnumerable<object>
@{ 
var modelType = Model.GetType().GenericTypeArguments[0];
var modelMetaData = ModelMetadataProviders.Current.GetMetadataForType(null, modelType.UnderlyingSystemType);

var propertiesToShow = modelMetaData.Properties.Where(p => p.ShowForDisplay);
var propertiesOfModel = modelType.GetProperties();

var tableData = propertiesOfModel.Zip(propertiesToShow, (columnName, columnValue) => new { columnName.Name, columnValue.PropertyName });
}

In my view, I simply call @Html.DisplayForModel() and the template gets loaded. There is no need to specify 'UIHint' on models.

I hope this was of some value.

swazza85
  • 721
  • 4
  • 10
  • 1
    I believe your `type.IsAssignableFrom(typeof(IEnumerable))` is backwards and should be `typeof(IEnumerable).IsAssignableFrom(type)` – Jeremy Cook Apr 17 '15 at 16:17
  • Is there a reason this isn't the selected answer? This is better than creating custom List classes everywhere or requiring UiHint on every enumerable property. – James Haug Feb 10 '17 at 22:08
7

In my question about not getting output from views, I actually have an example of how to template a model with a collection of child models and have them all render.

ASP.NET Display Templates - No output

Essentially, you need to create a model that subclasses List<T> or Collection<T> and use this:

@model ChildModelCollection 

@foreach (var child in Model)
{
    Html.DisplayFor(m => child);
}

In your template for the collection model to iterate and render the children. Each child needs to strongly-typed, so you may want to create your own model types for the items, too, and have templates for those.

So for the OP question:

public class ChildModelCollection : Collection<ChildModel> { }

Will make a strongly-typed model that's a collection that can be resolved to a template like any other.

Community
  • 1
  • 1
Luke Puplett
  • 42,091
  • 47
  • 181
  • 266
  • +1: making templates for lists implies that your project is growing up, then you should strongly type all collections. For me, this is the answer. `[UIHint]` is not type safe, simply search the most appropriate template. – T-moty Apr 22 '15 at 09:31
3

The actual "valid answer" is -IMHO- not correctly answering the question. I think the OP is searching for a way to have a list template that triggers without specifying the UIHint.

Magic stuff almost does the job

Some magic loads the correct view for a specified type.
Some more magic loads the same view for a collection of a specified type.
There should be some magic that iterates the same view for a collection of a specified type.

Change the actual behavior?

Open your favorite disassembler. The magic occurs in System.Web.Mvc.Html.TemplateHelpers.ExecuteTemplate. As you can see, there are no extensibility points to change the behavior. Maybe a pull request to MVC can help...

Go with the actual magic

I came up with something that works. Create a display template ~/Views/Shared/DisplayTemplates/MyModel.cshtml.

Declare the model as type object.

If the object is a collection, iterate and render the template again. If it's not a collection, then show the object.

@model object

@if (Model is IList<MyModel>)
{
    var models = (IList<MyModel>)Model;
<ul>
    @foreach (var item in models)
    {
@Html.Partial("DisplayTemplates/MyModel", item)
    }
</ul>
} else {
    var item = (MyModel)Model;
    <li>@item.Name</li>
    }
}

Now DisplayFor works without UIHint.

SandRock
  • 5,276
  • 3
  • 30
  • 49