Search code examples
c#-4.0razorasp.net-mvc-4mvc-editor-templates

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)

Solution

  • 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.