Не удается получить имя элемента управления из выражения с помощью ExpressionHelper

Я создаю помощник, который позволит мне создавать каскадные раскрывающиеся списки, которые заполняются с помощью AJAX. Вспомогательный метод выглядит так:

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);
}

Первое выражение нацелено на свойство, которое будет привязано к элементу управления, и у меня нет проблем с получением для него атрибутов id и name. Второй нацелен на свойство, из которого помощник будет каскадировать, но когда я прохожу через метод GetControlName, ExpressionHelper.GetExpressionText (выражение) возвращает пустую строку вместо имени свойства. Я добавил часы на "выражение", чтобы проверить, что пошло не так, и его значение следующее:

{model => Convert(model.TopCategoryId)}

Хотя я получаю следующее значение, когда получаю имя свойства для первого выражения:

{model => model.CategoryId}

Я действительно не понимаю, почему между этими двумя выражениями есть разница. Вот как я вызываю помощника на мой взгляд, если он так или иначе актуален:

@Html.AjaxSelectFor(model => model.CategoryId, model => model.TopCategoryId, "/api/Categories/GetSelectList", true)

Есть идеи, что здесь происходит?


person ZipionLive    schedule 21.12.2016    source источник
comment
Вы понимаете, что это никогда не даст вам правильного двухстороннего связывания модели и никакой проверки на стороне клиента.   -  person    schedule 21.12.2016
comment
Нет, не знал. На самом деле у меня нет большого опыта создания собственных помощников по привязке данных. У вас есть полезная документация по этому поводу?   -  person ZipionLive    schedule 21.12.2016
comment
Во-первых, проблема заключается в том, что TProperty в expression совпадает с TProperty в сигнатуре метода (ОК), но TProperty в cascadeFrom - не так, поэтому его нельзя решить.   -  person    schedule 22.12.2016
comment
Я настоятельно рекомендую вам изучить источник код для HtmlHelper методов, прежде чем пытаться написать свои собственные, и любые методы расширения, которые вы пишете, должны по возможности использовать существующие встроенные методы.   -  person    schedule 22.12.2016
comment
Все, что вам кажется, это сгенерировать дополнительные атрибуты, поэтому вам следует просто использовать return html.DropDownListFor(...) с одной из перегрузок accept htmlAttributes. Простой пример см. В этом ответе   -  person    schedule 22.12.2016


Ответы (1)


После того, как я какое-то время использовал хитрый обходной путь, я наконец понял это. Как указал Стивен Мьюке, проблема возникла из-за использования типа TProperty как для «выражения», так и для «cascadeFrom». Итак, вот как правильно (ну вроде как) решить эту проблему:

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)
{
    [...]
}

Надеюсь, это может кому-то помочь!

[Редактировать]

Кстати, вот код jQuery, чтобы это работало:

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));
    });
};
person ZipionLive    schedule 13.01.2017