ASP.net MVC - Display Template for a collection
Asked Answered
C

4

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?

Coben answered 3/11, 2011 at 21:23 Comment(0)
M
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>
Mousy answered 3/11, 2011 at 21:31 Comment(12)
Is there really no way to declare a template that would auto hook up as the default for any IEnumerable?Swearword
You could use the object.cshtml template for that.Mousy
can you hook up to object.cshtml in such a way that the default behavior is maintained except for my one special type?Swearword
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 usedPiderit
@Jack0fshad0ws, you are absolutely right. And that's the reason why in my answer I used @model IEnumerable<ChildModel> in the template. Because when you specify a template name, the model that is passed to the template is the collection property.Mousy
Not sure what advantage this provides over @Html.Partial, though.Splat
@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.Mousy
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.Robey
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.Holding
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<yourmodelclass> and name the display template (partial) exactly as you have your model class named.. with the exception of the leading underscore.Leupold
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.Leupold
Old comment but Kenny is right. If your display template is for FooViewModel and your view uses IEnumerable<FooViewModel>. Calling DisplayForModel() will display the template for each object in the collection.Bhatt
C
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.

Cammack answered 3/8, 2013 at 18:5 Comment(2)
I believe your type.IsAssignableFrom(typeof(IEnumerable<object>)) is backwards and should be typeof(IEnumerable<object>).IsAssignableFrom(type)Typhon
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.Garnetgarnett
M
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.

Minx answered 24/4, 2014 at 20:6 Comment(1)
+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.Reproval
S
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.

Strode answered 13/12, 2014 at 23:29 Comment(1)
You could move the <li> element tags in the foreach, to pair it with ul.Innis

© 2022 - 2024 — McMap. All rights reserved.