Rory McCrossan
Rory McCrossan

Reputation: 337560

Iterating over properties of a lambda expression

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.

Upvotes: 8

Views: 2635

Answers (4)

mclark1129
mclark1129

Reputation: 7592

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.

Upvotes: 2

Yogiraj
Yogiraj

Reputation: 1982

Consider this:

Create App_Code folder

Put the razor helper file Templates.cshtml.

It looks like below:

 @helper Print(string myvalue,string special="")
 {
   <pre> <input id="id" type="text" value ="@myvalue" data-val="@special"/> </pre>
 }

This way you dont have to write HTML in C# files. It is very handy.

Authors.cshtml looks like below:

@model IEnumerable<MvcApplication1.Models.Author>
@{
   ViewBag.Title = "Authors";
}

<h2>Authors</h2>

@foreach(var auth in Model)
{
   @Templates.Print(auth.Name);    
}

books.cshtml looks like below:

@model IEnumerable<MvcApplication1.Models.Book>
@{
    ViewBag.Title = "Books";
 }

<h2>Books</h2>

@foreach(var book in Model)
{
   @Templates.Print(book.Title,book.ISBN);    
}

Plugin all the special properties as you need per model class. If it gets too complicated, look into dynamic and expando object. It depends on how complex your models/viewmodels are.

Upvotes: 1

default.kramer
default.kramer

Reputation: 6103

Perhaps a builder-style API might simplify things:

@(Html.MyHelper(p)
    .Add(p => p.Author.Age)
    .Add(p => p.Author.LastName, "Last Name")
    .Build())

Notice that this allows you to add optional params in case you need them.

The code would look something like this

public static class Test
{
    public static Helper<TModel> MyHelper<TModel>(this HtmlHelper helper, TModel model)
    {
        return new Helper<TModel>(helper, model);
    }
}

public class Helper<TModel>
{
    private readonly HtmlHelper helper;
    private readonly TModel model;

    public Helper(HtmlHelper helper, TModel model)
    {
        this.helper = helper;
        this.model = model;
    }

    public Helper<TModel> Add<TProperty>(Expression<Func<TModel, TProperty>> expression)
    {
        // TODO
        return this;
    }

    public MvcHtmlString Build()
    {
        return new MvcHtmlString("TODO");
    }
}

Upvotes: 1

Jon Skeet
Jon Skeet

Reputation: 1500505

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.

Upvotes: 2

Related Questions