Search code examples
c#ienumerablehtml-helper

Can I create a HtmlHelper specifically for IEnumerable properties?


I would like to create a HtmlHelper that can be used on IEnumerable properties.

The aim is to use it like this:

@Html.DisplayForEnumerable(m => m.EnumerableItemsProperty, "ViewTemplateName");

If possible I would like to use the m => m.Items lambda syntax (as opposed to passing through Model.Items).

This is my best effort so far. But I'm not sure how to get the items variable from the expression parameter.

I suspect I may have to use something like IEnumerable<TValue> as the return type of the expression, but I'm quite new to generics and I've no idea how to implement this.

public static MvcHtmlString DisplayForEnumerable<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable>> expression, string templateName, object additonalViewData = null)
{
    var sb = new StringBuilder();

    // how to get items variable?

    foreach (var item in items)
    {
        var item1 = item;
        sb.Append(html.DisplayFor(m => item1, templateName, additonalViewData));
    }

    return MvcHtmlString.Create(sb.ToString());
}

Update

To clarify - I am taking this approach because I would like to be able so specify differnt templates for the same model. And the normal DisplayFor() enumeration does not occur if you specify a particular template.

I know I could just enumerate through manually, but I'd rather use this method unless someone more knowledgable advises otherwise.


Solution

  • You helper will need to be

    public static MvcHtmlString DisplayForEnumerable<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string templateName, object additionalViewData = null)
    {
        ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
        IEnumerable collection = metaData.Model as IEnumerable;
        if (collection == null)
        {
            return helper.DisplayFor(expression, templateName, additionalViewData );
        }
        StringBuilder html = new StringBuilder();
        foreach (var item in collection)
        {
            html.Append(helper.DisplayFor(m => item, templateName, additionalViewData).ToString());
        }
        return MvcHtmlString.Create(html.ToString());
    }
    

    Note the code allows you to pass either a single T or IEnumerable<T> (although the method name now does not really make sense). If you wanted to limit it to only IEnumerable<T> you could throw an InvalidCastException if collection == null

    Note that this approach will not work if you wanted to generate form controls for a collection (for example a EditorForEnumerable() method) because the required collection indexers will not be added to the generate name attributes. A better approach is to use the built-in DisplayFor() and EditorFor() methods which will generate the correct html for both T and IEnemerable<T>

    Assuming you have a Person.cs class, create a partial view in /Views/Shared/DisplayTemplates/Person.cshtml (note the name of the file must match the name of the class) and in the view simply use

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

    You can also create specific display and editor templates for each controller, for example /Views/yourControllerName/DisplayTemplates/Person.cshtml. This allows you to use one template in /Persons/Index and another template in /Organisation/Details/1 which might display a list of Person associated with an Organisation

    And finally, if you do need 2 different templates for Person in the same controller, you can use view models, for example class PersonVM and class AssociatedPersonVM and create an EditorTemplate for each