Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.net MVC - Display Template for a collection

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?

like image 288
Dismissile Avatar asked Nov 03 '11 21:11

Dismissile


4 Answers

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>
like image 64
Darin Dimitrov Avatar answered Oct 18 '22 07:10

Darin Dimitrov


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.

like image 42
swazza85 Avatar answered Oct 18 '22 06:10

swazza85


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.

like image 21
Luke Puplett Avatar answered Oct 18 '22 06:10

Luke Puplett


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.

like image 43
SandRock Avatar answered Oct 18 '22 06:10

SandRock