Iterating over properties of a lambda expression

.net c# expression-trees lambda

Question

I am attempting to genericise a complex control which is used in my website quite often but with different fields. The functionality in the control is always the same, it's just the underlying fields which change.

To achieve the method of showing different fields I am attempting to create a HTMLHelper extension which accepts a Expression<Func<TModel,TProperty>> as a parameter, which would contain the properties of a class required for display in the control. For example:

The view:

@model Project.Core.Page
@Html.MyHelper(p => new { p.Author.Name, p.Author.Location, p.Author.Age });

It's the extension I'm having problems with - how can I iterate over the provided params in the lambda to provide each with a TextBoxFor(), or even manually create a input element and populate it with the value and name of the lambda parameter?

The extention in psuedo:

public static MvcHtmlString MyHelper<TModel,TProperty>(
    this HtmlHelper<TModel> helper, 
    Expression<Func<TModel,TProperty>> expression) {
    foreach (var parameter in expression.???) {
        // helper.TextBoxFor(???) 
        // TagBuilder("input").Attributes("name", expression.???)
    }
}

I feel like I've been staring at this for far too long, and I'm also feeling there's a more simple way I'm overlooking of achieving this.

Any help is greatly appreciated. If you need further details, or I've missed something important, let me know.

Accepted Answer

If you assume the following:

  1. The result of the input expression is a projection (returns a new object, anonymous or otherwise)
  2. The elements of the projection are all MemberExpressions, and do not contain a call to a method on the model or on its children

Then you can achieve what you want by using the following approach:

Edit:

After realizing that my first example could not handle objects with complex properties, I updated the code to use a helper method to access property values. This method traverses over the property chain using recursion to return the appropriate values.

public static MvcHtmlString MyHelper<TModel,object>(
    this HtmlHelper<TModel> helper, 
    Expression<Func<TModel,object>> expression) {

        var newExpression = expression.Body as NewExpression;
        TModel model = helper.ViewData.Model;

        foreach (MemberExpression a in newExpression.Arguments) {

            var propertyName = a.Member.Name;
            var propertyValue = GetPropertyValue<TModel>(model, a);

            // Do whatever you need to with the property name and value;

        }

    }

    private static object GetPropertyValue<T>(T instance, MemberExpression me) {

        object target;

        if (me.Expression.NodeType == ExpressionType.Parameter) {
            // If the current MemberExpression is at the root object, set that as the target.            
            target = instance;
        }
        else {                
            target = GetPropertyValue<T>(instance, me.Expression as MemberExpression);
        }

        // Return the value from current MemberExpression against the current target
        return target.GetType().GetProperty(me.Member.Name).GetValue(target, null);

    }

Note: I did not implement this directly as a MVC extension method in my IDE, so a slight variation of the syntax may be required.


Popular Answer

The expression you'll have created will be relatively complicated - it'll need to fetch all the properties and then call the anonymous type constructor. "Disassembling" that may get painful... although if you still want to try, I'd suggest just leaving an empty method implementation and looking in the debugger to see what the expression looks like.

If you'd be happy to settle for a slightly uglier form of calling code, it would be much simpler to implement this:

@Html.MyHelper(p => p.Author.Name, p => p.Author.Location, p => p.Author.Age);

You could make that take a params Expression<TModel, object> or you could declare multiple overloads with different numbers of parameters, e.g.

// Overload for one property
MyHelper<TModel, TProperty1>(this ..., Expression<Func<TModel, TProperty1>> func1)

// Overload for two properties
MyHelper<TModel, TProperty1, TProperty2>(this ...,
   Expression<Func<TModel, TProperty1>> func1,
   Expression<Func<TModel, TProperty2>> func2)

etc.



Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why