在扩展方法中玩转Linq表达式

6
我编写了一个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);
    }

虽然这个功能运作得非常好,但有时候我想要更进一步。我希望能够自定义此元素的id,display和title部分,而不必手动编写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)

在这种情况下,选项的标题属性应为itemName属性和itemDescription属性的连接。我承认lambda表达式和Linq函数的元级别让我有点晕眩。有人可以指引我正确的方向吗?

最终结果对于那些好奇的人,以下代码使用lambda表达式完全控制了选择列表的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);

1
注意:ToStringOrEmpty() 扩展方法是我的自定义方法,它接受一个对象并对其执行 ToString() 操作,除非它为 null,在这种情况下,它返回一个空字符串。 - Jeremy Holovacs
1个回答

6
如果您不需要在各个选项上使用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
)

提供的第一个选项正是我想避免的。我想完全控制代码,不想将它发送到其他人的代码中...由于指定的原因,我失去了做许多很酷的事情的能力。你的第二个想法对我要做的事情有很大的帮助...我会尝试这个概念,看看能否使其工作。 - Jeremy Holovacs
扩展方法中的htmlHelper返回语句会给出一个ierror,指示System.Web.Mvc.HtmlHelper<TModel>不包含DropDownListFor的定义,也没有扩展方法blah blah。 - Lord of Scripts
这是因为您没有将定义此扩展方法的必要命名空间引入范围内:using System.Web.Mvc.Html - Darin Dimitrov
很好,但如何在选择框上添加客户端验证? - Timeless
@Timeless,为什么要验证选择列表?您已经提供了值,没有用户输入需要检查。您应该防止垃圾数据成为用户选择的选项。 - Jeremy Holovacs
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接