1

Scenario

public class Element {
   public int Id {get;set;}
}

public class ViewModel {
   public IList<Element> Elements{get;set;}
}

I have a method with a parameter of type Expression<Func<Element, int>>, which looks like m => m.Id

I'd like to transform

m => m.Id (where m is an Element)

to

x => x.Elements[0].Id where x is a ViewModel, and 0 is an "index" parameter

What I have now (It's of course generic, I removed the generic part for clarity)

public static class Helpers {
    public static Expression<Func<ViewModel, int>> BuildExpressionArrayFromExpression(
                this Expression<Func<Element, int>> expression,
                ViewModel model,
                int index = 0, 
                string bindingPropertyName = "Elements"//the name of the "List" property in ViewModel class
                ) 
    {
       var parameter = Expression.Parameter(typeof(ViewModel), "x");
       var viewModelProperty = model.GetType().GetProperty(bindingPropertyName);
       Expression member = parameter;//x => x
       member = Expression.Property(member, viewModelProperty);//x => x.Elements

       var test1 =  Expression.Property(member, "Item", new Expression[]{Expression.Constant(index)});
       //x => x.Elements.Item[0], and I don't want Item

       var test2 = Expression.Call(member, viewModelProperty.PropertyType.GetMethod("get_Item"), new Expression[] {Expression.Constant(index)});
       //x 0> x.Elements.get_Item(0), and I don't want get_Item(0)

       //code to add Id property to expression, not problematic
       return Expression.Lambda<Func<ViewModel, int>(member, parameter);
    }
}

EDIT

I need x => x.Elements[0] and not x => x.Elements.Item[0], because the resulting expression must be called with an InputExtensions.TextBoxFor(<myIndexedExpression>)

Imagine a class like that

public class Test {
  public int Id {get;set;}
  public IList<Element> Elements {get;set;}
}

and a Post Action

[HttpPost]
public ActionResult Edit(Test model) {
 bla bla bla.
}

If the name attributes of my inputs are not well generated, I then have binding problems (model.Elements is empty in my Post Action).

Name attribute of my input should be

Elements[0]PropertyName

and I get (depending on my tries)

PropertyName

or (maybe not exact, I try to reproduce this case)

Elements.Item[0].PropertyName

EDIT2

Also tried a different solution, working with ViewData.TemplateInfo.HtmlFieldPrefix, but I then get

Elements.[0].PropertyName

(and Elements_0_PropertyName as Id).

The first dot is unwanted in name, and the first "double underscore" should be a simple one in id.

I actually use this solution, working with regex (argh) to remove the unwanteds . and _ , but I'd like to avoid this.

Raphaël Althaus
  • 59,727
  • 6
  • 96
  • 122
  • Where are you use `Expression> expression` on which you call extension? – Hamlet Hakobyan Mar 13 '13 at 19:17
  • @HamletHakobyan a little bit long to explain, but it's for an editable Grid helper. If the "list" element is the model, no problem. If it's a part of ViewModel (what I'm facing now), and I want to bind the model including the grid elements into my post action, I need to resolve this... – Raphaël Althaus Mar 13 '13 at 19:23
  • You don't understand me. You make extension on type `Expression>` but you don't use your extended object in your extension. – Hamlet Hakobyan Mar 13 '13 at 19:28
  • @HamletHakobyan oh... Well, I use it at the end of my extension (not shown and not problematic code) (`m => m.a.b.Id` should become `x => x.list[0].a.b.Id` : I take `a.b.Id` from `Expression>` – Raphaël Althaus Mar 13 '13 at 19:47

3 Answers3

6

This is just a matter of the string representation of the expression tree, and you don't get to change that. The expression tree you're building is fine. You can see the same effect if you use a lambda expression to build the expression tree:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

class Test
{
    public static void Main()
    {
        Expression<Func<List<string>, string>> expression = list => list[0];
        Console.WriteLine(expression);
    }
}

Output:

list => list.get_Item(0)

I'd be very surprised if the result of calling ToString() on the expression tree was really the problem you're facing. Instead of telling us the result you think you need and the vague "I need it for MVC binding reasons" justification, you should explain what's actually going wrong. I strongly suspect the problem isn't where you think it is.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • You're perfectly right, my "binding reasons" is not clear at all. The real problem is that I try to use InputExtensions on these generated expressions : `myHelper_.TextBoxFor(myArrayExpression)` : and that's where I've got problems, as id and name are not well generated, and that's why I have then binding problems... Is that more clear ? – Raphaël Althaus Apr 25 '13 at 09:23
  • 1
    Oh oh. Think I missed something. Actually, it seems to work using `Expression.Call(member, viewModelProperty.PropertyType.GetMethod("get_Item"), new Expression[] {Expression.Constant(index)});` but not with `Expression.Property(member, "Item", new Expression[]{Expression.Constant(index)});` (InputExtensions use `ExpressionHelper.GetExpressionText(expression)` and the result is really different from one to the other. And I also think that when I asked, I used MVC3, and now I'm on MVC4. Maybe the reason why it's working, must admit I'm surprised... – Raphaël Althaus Apr 25 '13 at 11:01
  • You need to be a *lot* clearer about what counts as "working" and what counts as "not working". You say that `InputExtensions` use `GetExpressionText()`, but you've given no indication of *how* it's used. I wouldn't want to use the textual representation of an expression tree for anything other than diagnostics. – Jon Skeet Apr 25 '13 at 11:54
  • Well, I just use `public static MvcHtmlString TextBoxFor(this HtmlHelper htmlHelper, Expression> expression);` (with my expression as parameter), which is a method of the InputExtensions class in System.Web.Mvc.Html. And looking at the source code, I noticed that it's using `GetExpressionText` on expression to build the name and id of my input. So I tested my solutions, passing my expression to `ExpressionHelpers.GetExpressionText(LambdaExpression expression)` and noticed a difference depending on the method used to create my expression. – Raphaël Althaus Apr 25 '13 at 12:17
  • @RaphaëlAlthaus: With your update - is the *only* problem that the name attribute isn't being generated properly in the resulting HTML? And how have you determined what "properly" really means here? The whole question still seems very confused to me. – Jon Skeet Apr 25 '13 at 12:24
  • Well, for the first question, yes, it's only that, but it's impacting model binding and validation errors... And the "properly" is a comparison of what's generated by a simple case as you can find here, which binds properly : http://stackoverflow.com/questions/14165632/asp-net-mvc-4-for-loop-posts-model-collection-properties-but-foreach-does-not – Raphaël Althaus Apr 25 '13 at 12:40
  • @RaphaëlAlthaus: Okay, well I think this is so caught up in MVC binding that I'm not going to be able to help you further. But I *would* advise you to try to find a solution where you can either override the attribute name, or something similar so that the text representation of the expression tree is irrelevant. Anything else is going to be very fragile. – Jon Skeet Apr 25 '13 at 12:49
  • Yes, you're right, I will look for alternatives. Anyway, thanks (really) for having taken time to answer to this (dark and unclear) question ! – Raphaël Althaus Apr 25 '13 at 12:53
2
Expression<Func<Element, int>> expr1 =
    m => m.Id;
Expression<Func<ViewModel, Element>> expr2 =
    x => x.Elements[0];

Expression<Func<ViewModel, int>> result =
    expr1.ComposeWith(expr2);

Result:

expr1 = m => m.Id
expr2 = x => x.Elements.get_Item(0)
result = x => x.Elements.get_Item(0).Id

It replaces the parameter of expr1 (m) with the body of expr2 (x.Elements[0]), and replaces the input parameter with that from expr2 (x).

Extension method ComposeWith:

public static class FunctionalExtensions
{
    public static Expression<Func<TInput,TResult>> ComposeWith<TInput,TParam,TResult>(
        this Expression<Func<TParam,TResult>> left, Expression<Func<TInput,TParam>> right)
    {
        var param = left.Parameters.Single();

        var visitor = new ParameterReplacementVisitor(p => {
            if (p == param)
            {
                return right.Body;
            }
            return null;
        });

        return Expression.Lambda<Func<TInput,TResult>>(
            visitor.Visit(left.Body),
            right.Parameters.Single());
    }

    private class ParameterReplacementVisitor : ExpressionVisitor
    {
        private Func<ParameterExpression, Expression> _replacer;

        public ParameterReplacementVisitor(Func<ParameterExpression, Expression> replacer)
        {
            _replacer = replacer;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            var replaced = _replacer(node);
            return replaced ?? node;
        }
    }
}
Markus Jarderot
  • 86,735
  • 21
  • 136
  • 138
1

You should be able to use MakeIndex to make the indexer expression:

MemberExpression memberExpr = Expression.Property(member, viewModelProperty);//x => x.Elements
var indexProperty = typeof(IList<Element>).GetProperty("Item");
var indexExpr = Expression.MakeIndex(memberExpr, indexProperty, new Expression[]{Expression.Constant(index)});

return Expression.Lambda<Func<ViewModel, int>(indexExpr, parameter);
Lee
  • 142,018
  • 20
  • 234
  • 287
  • Thx, but this throws an ArgumentException on `Expression.MakeIndex` `Method 'System.Collections.Generic.IList1[Element] get_Elements()' declared on type 'ViewModel' cannot be called with instance of type 'System.Collections.Generic.IList1[Element]'` – Raphaël Althaus Mar 13 '13 at 19:10
  • @RaphaëlAlthaus - I think the index property info was wrong. I've updated the code to use the `IList.Item` property. Does that fix the issue? – Lee Mar 13 '13 at 19:14
  • well, no more exception, but I got... what I had in my test1 : `x => x.Elements.Item[0]`. Item is back. It's a valid expression, but I would like to avoid `Item`. It's maybe not possible, but... – Raphaël Althaus Mar 13 '13 at 19:19
  • @RaphaëlAlthaus: It sounds like all you're quibbling about is the result of calling `ToString` on the expression. you're not going to be able to change that, and I'd be really surprised if that genuinely affected the MVC binding. – Jon Skeet Apr 24 '13 at 17:31