JOEL'S CODE


Tags


JOEL'S CODE

Use MVC Templates with Dynamic Members with custom HtmlHelper Extensions

18th April 2011

The MVC template extension method helpers, EditorFor, DisplayFor, and LabelFor, are great. Based on some metadata they allow you to use templates for the data type provided in the metadata. For more on this see Brad Wilson’s Blog Posts.

These helpers however do not allow you to use templates if you do not know the name of the member at design time. For example: you may have a requirement to display custom fields, but may not know which fields to display nor the order to display them at design time. At runtime, however, you may have this list and need to build your fields on the fly. See simple example below.

<fieldset>  
    <legend>Person</legend>     

    @foreach (var item in Model.ColumnsToDisplay)     {         
        <div>             
            @Html.LabelFor("Person." + item)             
            @Html.EditorFor("Person." + item)                 
        </div>     
    }

</fieldset>

To accomplish this I created matching EditorFor, DisplayFor, LabelFor extension methods that take a string expression instead of an Expression<Func<TModel, TValue>>.  For this post I will use the basic EditorFor HtmlHelper extension method as my example, but I have implemented the others in the source code at the bottom of this post.

In the custom extension methods I am using the DynamicParser used in the Dynamic Query code sample. This creates an expression based on type of the model for the view and the expression string provided.

I wanted to use the existing extension method’s logic to get the editor template.

public static MvcHtmlString EditorFor<TModel>(  
    this HtmlHelper<TModel> htmlHelper, 
    Expression<Func<TModel, TValue>> expression)

However I could not call the existing extension method because it can not infer the TModel generic parameter type based on the expression I generated. So in order to call the existing extension method I used reflection. Here is the method below.

public static MvcHtmlString EditorFor<TModel>(  
        this HtmlHelper<TModel> htmlHelper, 
        string expression)
{

    var lambda = DynamicExpression.ParseLambda( typeof(TModel),
                                                expression);

    var funcType = typeof(Func<,>).MakeGenericType( typeof(TModel), 
                                                    lambda.Body.Type);

    var expType = typeof(Expression<>).MakeGenericType(funcType);

    var methods = 
        typeof(System.Web.Mvc.Html.EditorExtensions).GetMethods();        

    var method = methods.Where(
                    x => {
                        var args = x.GetGenericArguments();  
                        var parms = x.GetParameters();

                        return (   args.Count() == 2     
                                && args[0].Name == "TModel"
                                && args[1].Name == "TValue" 
                                && x.Name == "EditorFor"
                                && parms.Count() == 2
                                && parms[0].ParameterType.Name
                                    == "HtmlHelper`1"
                                && parms[1].ParameterType.Name 
                                    == "Expression`1"
                                );
                          }
    ).First();  

    method = method.MakeGenericMethod(
                        new Type[] { typeof(TModel), 
                        lambda.Body.Type });
    return (MvcHtmlString)method.Invoke(
                        null, 
                        new object[] { htmlHelper, lambda });

}

As you can see I had to create a generic instance of the extension method using the TModel type and the body type of the lambda expression I created. I then simply executed the method and returned the result.

Now I can my code from the beginning of the post will work.

<fieldset>  
    <legend>Person</legend>     

    @foreach (var item in Model.ColumnsToDisplay)     {         
        <div>             
            @Html.LabelFor("Person." + item)             
            @Html.EditorFor("Person." + item)                 
        </div>     
    }

</fieldset>

In my next blog post I will discuss how to use this same technique to use the extension methods when you do not know the model type at all at design time.

MVCTemplatesForRuntimeKnownMembers.zip (609.56 kb)

Joel Duvall
AUTHOR

Joel Duvall