Q:

Can't get control name from expression with ExpressionHelper

I'm creating a helper that will allow me to create cascading drop down lists that fill themselves using AJAX. The helper method looks like this :
public static MvcHtmlString AjaxSelectFor<TModel, TProperty>(
    this HtmlHelper<TModel> html,
    Expression<Func<TModel, TProperty>> expression,
    Expression<Func<TModel, TProperty>> cascadeFrom,
    string sourceUrl,
    bool withEmpty = false)
{
    string controlFullName = html.GetControlName(expression);
    string cascadeFromFullName = html.GetControlName(cascadeFrom);

    var selectBuilder = GetBaseSelect(controlFullName.GetControlId(), controlFullName, sourceUrl, withEmpty);
    selectBuilder.Attributes.Add("data-selected-id", html.GetValue(expression));
    selectBuilder.Attributes.Add("data-cascade-from", "#" + cascadeFromFullName.GetControlId());

    return new MvcHtmlString(selectBuilder.ToString());
}

private static TagBuilder GetBaseSelect(string controlId, string controlName, string sourceUrl, bool withEmpty)
{
    var selectBuilder = new TagBuilder("select");
    selectBuilder.Attributes.Add("id", controlId);
    selectBuilder.Attributes.Add("name", controlName);
    selectBuilder.Attributes.Add("data-toggle", "ajaxSelect");
    selectBuilder.Attributes.Add("data-source-url", sourceUrl);
    selectBuilder.Attributes.Add("data-with-empty", withEmpty.ToString());
    selectBuilder.AddCssClass("form-control");
    return selectBuilder;
}

internal static string GetControlName<TModel, TProperty>(this HtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression)
{
    string controlName = ExpressionHelper.GetExpressionText(expression);
    return html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(controlName);
}

internal static string GetControlId(this string controlName)
{
    return TagBuilder.CreateSanitizedId(controlName);
}

The first expression targets the property that will be bound in the control and I have no problem getting the id and name attributes for it. The second targets the property that the helper will cascade from, but when I get through the GetControlName method, ExpressionHelper.GetExpressionText(expression) returns an empty string instead of the property name. I added a watch on "expression" to check what was going wrong, and its value is as follows :
{model => Convert(model.TopCategoryId)}

While I get the following value when I'm getting the property name for the first expression :
{model => model.CategoryId}

I really don't understand why there is a difference between the two expressions. Here's how I call the helper on my view, in case it's relevant anyhow :
@Html.AjaxSelectFor(model => model.CategoryId, model => model.TopCategoryId, "/api/Categories/GetSelectList", true)

Any idea what's going on here ?

A:

After having used a hacky workaround for some time, I finally figured it out. As Stephen Muecke pointed out, the problem came from using the type TProperty for both "expression" and "cascadeFrom". So, here's how to properly (well, kind of) solve this problem :
public static MvcHtmlString AjaxSelectFor<TModel, TProperty, TCascadeProperty>(
    this HtmlHelper<TModel> html,
    Expression<Func<TModel, TProperty>> expression,
    Expression<Func<TModel, TCascadeProperty>> cascadeFrom,
    string sourceUrl,
    bool withEmpty = false)
{
    [...]
}

Hope that might help someone !
[Edit]
By the way, here's the jQuery code to make this work :
var common = {};

$(document).ready(function() {
    common.bindAjaxSelect();
})

common.bindAjaxSelect = function () {
    $('[data-toggle="ajaxSelect"]').each(function () {
        common.clearSelect($(this));
    });
    $('[data-toggle="ajaxSelect"]').not('[data-cascade-from]').each(function () {
        common.fillAjaxSelect($(this));
        $(this).on('change', function () {
            common.bindAjaxSelectCascade('#' + $(this).attr('id'));
        });
    });
};

common.bindAjaxSelectCascade = function (selector) {
    $('[data-toggle="ajaxSelect"][data-cascade-from="' + selector + '"]').each(function () {
        common.fillAjaxSelect($(this), selector);
        $(this).unbind('change');
        $(this).on('change', function () {
            common.bindAjaxSelectCascade('#' + $(this).attr('id'));
        });
    });
};

common.fillAjaxSelect = function (select, cascadeFromSelector) {
    var controlId = select.attr('id');
    var sourceUrl = select.attr('data-source-url');
    var withEmpty = select.attr('data-with-empty');
    var selectedId = select.attr('data-selected-id');
    var parentId = $(cascadeFromSelector).val();
    var emptyCheck = withEmpty ? 1 : 0;

    $('[data-toggle="ajaxSelect"][data-cascade-from="#' + select.attr('id') + '"]').each(function () {
        common.clearSelect($(this));
    });

    var requestParameters = parentId === undefined
        ? { ajax: true, withEmpty: withEmpty }
        : { ajax: true, parentId: parentId, withEmpty: withEmpty };

    $.getJSON(sourceUrl, requestParameters, function (response) {
        if (response.Success === true) {
            if (response.Data.length > emptyCheck) {
                var options = [];
                $.each(response.Data, function (key, item) {
                    if (selectedId !== undefined && item.Id === selectedId) {
                        options.push('<option value="' + item.Id + '" selected>' + item.Value + '</option>');
                    } else {
                        options.push('<option value="' + item.Id + '">' + item.Value + '</option>');
                    }
                });
                select.html(options.join(''));
                select.enable();

                if (selectedId !== undefined && selectedId !== '') {
                    common.bindAjaxSelectCascade('#' + controlId);
                }
            } else {
                common.clearSelect(select);
            }
        } else {
            common.clearSelect(select);
            //TODO : append error message to page.
        }
    });
};

common.clearSelect = function (select) {
    select.disable();
    select.html('');
    $('[data-toggle="ajaxSelect"][data-cascade-from="' + select.attr('id') + '"]').each(function () {
        common.clearSelect($(this));
    });
};

