Веселье (?) с выражениями Linq в методах расширения

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

    public static HtmlString SelectFor<TModel, TProperty, TListItem>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        IEnumerable<TListItem> enumeratedItems,
        string idPropertyName,
        string displayPropertyName,
        string titlePropertyName,
        object htmlAttributes) where TModel : class
    {
        //initialize values
        var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        var propertyName = metaData.PropertyName;
        var propertyValue = htmlHelper.ViewData.Eval(propertyName).ToStringOrEmpty();
        var enumeratedType = typeof(TListItem);

        //build the select tag
        var returnText = string.Format("<select id=\"{0}\" name=\"{0}\"", HttpUtility.HtmlEncode(propertyName));
        if (htmlAttributes != null)
        {
            foreach (var kvp in htmlAttributes.GetType().GetProperties()
             .ToDictionary(p => p.Name, p => p.GetValue(htmlAttributes, null)))
            {
                returnText += string.Format(" {0}=\"{1}\"", HttpUtility.HtmlEncode(kvp.Key),
                 HttpUtility.HtmlEncode(kvp.Value.ToStringOrEmpty()));
            }
        }
        returnText += ">\n";

        //build the options tags
        foreach (TListItem listItem in enumeratedItems)
        {
            var idValue = enumeratedType.GetProperties()
             .FirstOrDefault(p => p.Name == idPropertyName)
             .GetValue(listItem, null).ToStringOrEmpty();
            var titleValue = enumeratedType.GetProperties()
             .FirstOrDefault(p => p.Name == titlePropertyName)
             .GetValue(listItem, null).ToStringOrEmpty();
            var displayValue = enumeratedType.GetProperties()
             .FirstOrDefault(p => p.Name == displayPropertyName)
             .GetValue(listItem, null).ToStringOrEmpty();
            returnText += string.Format("<option value=\"{0}\" title=\"{1}\"",
             HttpUtility.HtmlEncode(idValue), HttpUtility.HtmlEncode(titleValue));
            if (idValue == propertyValue)
            {
                returnText += " selected=\"selected\"";
            }
            returnText += string.Format(">{0}</option>\n", displayValue);
        }

        //close the select tag
        returnText += "</select>";
        return new HtmlString(returnText);
    }

... это работает плавно, но бывают моменты, когда я хочу пойти дальше. Я хотел бы настроить идентификатор, отображение и заголовок этого зверя без необходимости писать html. Например, если у меня есть несколько классов в такой модели:

public class item
{
    public int itemId { get; set; }
    public string itemName { get; set; }
    public string itemDescription { get; set; }
}

public class model
{
    public IEnumerable<item> items { get; set; }
    public int itemId { get; set; }
}

На мой взгляд, я могу написать:

@Html.SelectFor(m => m.itemId, Model.items, "itemId", "itemName", "itemDescription", null)

... и я получу хороший раскрывающийся список с атрибутами заголовка и т. д. Это здорово, если перечисленные элементы имеют свойства именно так, как я хотел бы их отобразить. Но то, что я действительно хотел бы сделать, это что-то вроде:

@Html.SelectFor(m => m.itemId, Model.items, id=>id.itemId, disp=>disp.itemName, title=>title.itemName + " " + title.itemDescription, null)

... и в этом случае атрибут title в параметрах должен быть конкатенацией свойства itemName и свойства itemDescription. Признаюсь, мета-уровень лямбда-выражений и функций Linq вызвал у меня легкое головокружение. Может ли кто-нибудь указать мне в правильном направлении?

КОНЕЧНЫЙ РЕЗУЛЬТАТ Для тех, кому интересно, следующий код дает мне полный контроль над свойствами ID, Title и DisplayText списка выбора с помощью лямбда-выражений:

    public static HtmlString SelectFor<TModel, TProperty, TListItem>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> forExpression,
        IEnumerable<TListItem> enumeratedItems,
        Attribute<TListItem> idExpression,
        Attribute<TListItem> displayExpression,
        Attribute<TListItem> titleExpression,
        object htmlAttributes,
        bool blankFirstLine) where TModel : class
    {
        //initialize values
        var metaData = ModelMetadata.FromLambdaExpression(forExpression, htmlHelper.ViewData);
        var propertyName = metaData.PropertyName;
        var propertyValue = htmlHelper.ViewData.Eval(propertyName).ToStringOrEmpty();
        var enumeratedType = typeof(TListItem);

        //build the select tag
        var returnText = string.Format("<select id=\"{0}\" name=\"{0}\"", HttpUtility.HtmlEncode(propertyName));
        if (htmlAttributes != null)
        {
            foreach (var kvp in htmlAttributes.GetType().GetProperties()
             .ToDictionary(p => p.Name, p => p.GetValue(htmlAttributes, null)))
            {
                returnText += string.Format(" {0}=\"{1}\"", HttpUtility.HtmlEncode(kvp.Key),
                 HttpUtility.HtmlEncode(kvp.Value.ToStringOrEmpty()));
            }
        }
        returnText += ">\n";

        if (blankFirstLine)
        {
            returnText += "<option value=\"\"></option>";
        }

        //build the options tags
        foreach (TListItem listItem in enumeratedItems)
        {
            var idValue = idExpression(listItem).ToStringOrEmpty();
            var displayValue = displayExpression(listItem).ToStringOrEmpty();
            var titleValue = titleExpression(listItem).ToStringOrEmpty();
            returnText += string.Format("<option value=\"{0}\" title=\"{1}\"",
                HttpUtility.HtmlEncode(idValue), HttpUtility.HtmlEncode(titleValue));
            if (idValue == propertyValue)
            {
                returnText += " selected=\"selected\"";
            }
            returnText += string.Format(">{0}</option>\n", displayValue);
        }

        //close the select tag
        returnText += "</select>";
        return new HtmlString(returnText);
    }

    public delegate object Attribute<T>(T listItem);

person Jeremy Holovacs    schedule 29.10.2011    source источник
comment
ПРИМЕЧАНИЕ. Расширение ToStringOrEmpty() — это мой метод, который берет объект и выполняет для него функцию ToString(), если только он не равен нулю, и в этом случае он возвращает пустую строку.   -  person Jeremy Holovacs    schedule 29.10.2011


Ответы (1)


Если вам не нужен атрибут title для отдельных опций, ваш код можно упростить до:

public static HtmlString SelectFor<TModel, TProperty, TIdProperty, TDisplayProperty, TListItem>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression,
    IEnumerable<TListItem> enumeratedItems,
    Expression<Func<TListItem, TIdProperty>> idProperty,
    Expression<Func<TListItem, TDisplayProperty>> displayProperty,
    object htmlAttributes
) where TModel : class
{
    var id = (idProperty.Body as MemberExpression).Member.Name;
    var display = (displayProperty.Body as MemberExpression).Member.Name;
    var selectList = new SelectList(enumeratedItems, id, display);
    var attributes = new RouteValueDictionary(htmlAttributes);
    return htmlHelper.DropDownListFor(expression, selectList, attributes);
}

и используется так:

@Html.SelectFor(
    m => m.itemId, 
    Model.items, 
    id => id.itemId, 
    disp => disp.itemName, 
    null
)

И если вам нужен атрибут title, вам придется реализовать все, что делает помощник DropDownList вручную, что может быть довольно болезненным. Вот пример лишь небольшой части всего функционала:

public static class HtmlExtensions
{
    private class MySelectListItem : SelectListItem
    {
        public string Title { get; set; }
    }

    public static HtmlString SelectFor<TModel, TProperty, TIdProperty, TDisplayProperty, TListItem>(
        this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        IEnumerable<TListItem> enumeratedItems,
        Expression<Func<TListItem, TIdProperty>> idProperty,
        Expression<Func<TListItem, TDisplayProperty>> displayProperty,
        Func<TListItem, string> titleProperty,
        object htmlAttributes
    ) where TModel : class
    {
        var name = ExpressionHelper.GetExpressionText(expression);
        var fullHtmlName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

        var select = new TagBuilder("select");
        var compiledDisplayProperty = displayProperty.Compile();
        var compiledIdProperty = idProperty.Compile();
        select.GenerateId(fullHtmlName);
        select.MergeAttributes(new RouteValueDictionary(htmlAttributes));
        select.Attributes["name"] = fullHtmlName;
        var selectedValue = htmlHelper.ViewData.Eval(fullHtmlName);
        var options = 
            from i in enumeratedItems
            select ListItemToOption(
                ItemToSelectItem(i, selectedValue, compiledIdProperty, compiledDisplayProperty, titleProperty)
            );
        select.InnerHtml = string.Join(Environment.NewLine, options);
        return new HtmlString(select.ToString(TagRenderMode.Normal));
    }

    private static MySelectListItem ItemToSelectItem<TListItem, TIdProperty, TDisplayProperty>(TListItem i, object selectedValue, Func<TListItem, TIdProperty> idProperty, Func<TListItem, TDisplayProperty> displayProperty, Func<TListItem, string> titleProperty)
    {
        var value = Convert.ToString(idProperty(i));
        return new MySelectListItem
        {
            Value = value,
            Text = Convert.ToString(displayProperty(i)),
            Title = titleProperty(i),
            Selected = Convert.ToString(selectedValue) == value
        };
    }

    private static string ListItemToOption(MySelectListItem item)
    {
        var builder = new TagBuilder("option");
        builder.Attributes["value"] = item.Value;
        builder.Attributes["title"] = item.Title;
        builder.SetInnerText(item.Text);
        if (item.Selected)
        {
            builder.Attributes["selected"] = "selected";
        }
        return builder.ToString();
    }
}

а затем используйте так:

@Html.SelectFor(
    m => m.itemId, 
    Model.items, 
    id => id.itemId, 
    disp => disp.itemName, 
    title => title.itemName + " " + title.itemDescription, 
    null
)
person Darin Dimitrov    schedule 29.10.2011
comment
Первый предоставленный вариант - это именно то, чего я пытаюсь избежать. Я хочу тотальный контроль над кодом, не хочу посылать его в чужой код... по указанной причине я теряю возможность делать много крутых вещей. Ваша вторая идея многообещающая для того, что я пытаюсь сделать... Я поиграю с этой концепцией и посмотрю, смогу ли я заставить ее работать. - person Jeremy Holovacs; 29.10.2011
comment
@JeremyHolovacs, я действительно не понимаю, какие крутые вещи вы теряете возможность делать со стандартными помощниками ASP.NET MVC. Ну да, вы не можете добавить title к этим тегам option, но действительно ли это то, что вам нужно? Лично я бы вообще не стал писать такие хелперы. Я бы просто разработал модели представления со свойством IEnumerable<SelectListItem> и передал бы его стандартному хелперу Html.DropDownListFor. Это делает работу. Если вы не хотите использовать модели представления и пытаетесь передать свои модели предметной области в представление и пытаетесь написать собственные помощники, ну, это другой вопрос. - person Darin Dimitrov; 29.10.2011
comment
Это абсолютно так. Большинство моих требований к дизайну требуют имени в раскрывающемся списке, но описания при наведении курсора. Более того, мне не нравится отсутствие option. Зачем соглашаться, если вам не нужно? - person Jeremy Holovacs; 29.10.2011
comment
Оператор возврата htmlHelper в методе расширения выдает ierror, указывающую, что System.Web.Mvc.HtmlHelper‹TModel› не содержит определения для DropDownListFor и метода расширения и прочее бла-бла. - person Lord of Scripts; 02.06.2012
comment
Это потому, что вы не внесли в область видимости необходимое пространство имен, в котором этот метод расширения определен: using System.Web.Mvc.Html. - person Darin Dimitrov; 03.06.2012
comment
отлично, но как добавить проверку на стороне клиента при выборе - person Timeless; 20.11.2012
comment
@Timeless, зачем вам проверять список выбора? Вы предоставляете значения, пользовательский ввод для проверки отсутствует. Вы должны предотвратить возможность выбора пользователем мусорных данных. - person Jeremy Holovacs; 05.01.2013