Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an EditorTemplate using UIHint with a property of type IEnumerable<T>

In MVC you can create an editor template for T and then when you want to render an editor for a property of type IEnumerable<T> you can simply do e.g.

Html.EditorFor(m => m.MyListOfT)

The beauty of this is that names are automatically created by the framework for the inputs, and then when posting back the model binding all works nicely.

My question is: how do you do the above when you have more than one type of editor template?

I've tried using UIHint(), however it only seems to allow you to specify the UIHint against the list, rather than each item in the list. This means you then have to create an EditorTemplate for the list, with a foreach() loop, and you then miss out on the nice auto-naming and model binding.

What am I missing here?

The Model is e.g.

public class MyViewModel
{
    public IEnumerable<SomeType> SomeProperty { get; set; }
}

Ideally I want to do something like:

public class MyViewModel
{
    [UIHint("SomeTypeTemplate")]
    public IEnumerable<SomeType> SomeProperty { get; set; }
}

and have that automatically apply to all elements in the list so I can render with just:

Html.EditorFor(m => m.SomeProperty)
like image 350
magritte Avatar asked Oct 09 '12 10:10

magritte


3 Answers

What am I missing here?

Nothing. Unfortunately that's how it is. If you specify the template name when calling the Html.EditorFor or using an UIHint the template will be called for the list and not for each element.

This being said you could of course write a custom extension method that will achieve this functionality:

public static class HtmlExtensions
{
    private class ViewDataContainer: IViewDataContainer
    {
        public ViewDataContainer(ViewDataDictionary viewData)
        {
            ViewData = viewData;
        }

        public ViewDataDictionary ViewData { get; set; }
    }

    public static IHtmlString EditorForCollection<TModel, TProperty>(
        this HtmlHelper<TModel> html, 
        Expression<Func<TModel, IList<TProperty>>> expression
    )
    {
        var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
        if (string.IsNullOrEmpty(metadata.TemplateHint))
        {
            return html.EditorFor(expression);
        }

        var collection = metadata.Model as IList<TProperty>;

        var sb = new StringBuilder();
        for (int i = 0; i < collection.Count; i++)
        {
            var indexExpression = Expression.Constant(i, typeof(int));
            var itemGetter = expression.Body.Type.GetProperty("Item", new[] { typeof(int) }).GetGetMethod();
            var methodCallExpression = Expression.Call(expression.Body, itemGetter, indexExpression);
            var itemExpression = Expression.Lambda<Func<TModel, TProperty>>(methodCallExpression, expression.Parameters[0]);
            var result = html.EditorFor(itemExpression, metadata.TemplateHint).ToHtmlString();
            sb.AppendLine(result);
        }
        return new HtmlString(sb.ToString());
    }
}

that could operate on view model properties of type collection which are decorated with the UIHint attribute:

public class MyViewModel
{
    [UIHint("SomeTypeTemplate")]
    public IList<ItemViewModel> Items { get; set; }
}

and in your view:

@model MyViewModel
@Html.EditorForCollection(x => x.Items)

and your ~/Views/Shared/EditorTemplates/SomeTypeTemplate.cshtml could now be typed to a single ItemViewModel:

@model ItemViewModel
...

You no longer need an intermediary display template in which you would simply be looping and calling the actual template - that would be a real waste.

like image 178
Darin Dimitrov Avatar answered Oct 16 '22 19:10

Darin Dimitrov


You create 2 EditorTemplates. One handles the IEnumerable, and this template loops to create an

Html.EditorFor(m => m.SomeProperty, "SomeIndividualTemplate")

for each item in the enumeration.

like image 3
simon831 Avatar answered Oct 16 '22 19:10

simon831


Although @Darin's answer is perfect, for some reasons which i am yet to find the TemplateHint from the ModelMetaData was always null. As as solution i created one more overload of @Darin's code to accept the template name parameter and render the View instead of checking for TemplateHint in ModelMetaData. Sorry to post it as answer as i don't have privileges to post as comment. Thanks Darin :)

like image 1
ManojAnavatti Avatar answered Oct 16 '22 18:10

ManojAnavatti