Fun (?) with Linq Expressions in extension methods
Asked Answered
S

1

6

I wrote an HtmlHelper expression I use a lot of the time to put title tags into my dropdown lists like so:

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

...this works swimmingly, but there are times when I want to go further. I'd like to customize the id, display, and title pieces of this beast without having to write out the html. For example, if I have some classes in a model like so:

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

In my view I can write:

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

...and I'll get a nice dropdown with title attributes etc. This is great as long as the enumerated items have properties exactly as I'd like to display them. But what I'd really like to do is something like:

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

...and have, in this case, the title attribute on the options be a concatenation of the itemName property and the itemDescription property. I confess the meta-level of lambda expressions and Linq functions has got me a little dizzy. Can someone point me in the right direction?

FINAL RESULT For those who are curious, the following code gives me complete control over the select list's ID, Title, and DisplayText properties using lambda expressions:

    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);
Semantic answered 29/10, 2011 at 2:19 Comment(1)
NOTE: the ToStringOrEmpty() extension is my method that takes an object and does a ToString() on it unless it's null, in which case it brings back an empty string.Semantic
S
6

If you don't need the title attribute on individual options your code could be simplified to:

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

and used like this:

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

And if you need the title attribute, well, you will have to implement everything that the DropDownList helper does manually which could be quite of a pain. Here's an example of only a small portion of all the functionality:

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

and then use like this:

@Html.SelectFor(
    m => m.itemId, 
    Model.items, 
    id => id.itemId, 
    disp => disp.itemName, 
    title => title.itemName + " " + title.itemDescription, 
    null
)
Shanteshantee answered 29/10, 2011 at 9:28 Comment(7)
The first option provided is exactly what I'm trying to avoid. I want total control of the code, don't want to send it to someone else's code... for the reason specified, I lose the ability to do a lot of cool things. Your second idea has a lot of promise for what I'm trying to do... I'll play with that concept and see if I can get that to work.Semantic
@JeremyHolovacs, I really don't see what cool things you are losing the ability to do with the standard ASP.NET MVC helpers. Well, yeah, you can't add a title to those option tags but is it really something you need? Personally I wouldn't bother to write such helpers at all. I would simply design view models that have IEnumerable<SelectListItem> property and throw it to the standard Html.DropDownListFor helper. It does the job. If you don't want to use view models and are trying to pass your domain models to the view and trying to write custom helpers, well, that's another question.Shanteshantee
It absolutely is. Most of my design requirements want a name in the dropdown but a description in the hover. More to the point, I don't like not having the option. Why settle when you don't have to?Semantic
The htmlHelper return statement in the extension method gives an ierror indicating that System.Web.Mvc.HtmlHelper<TModel> does not contain a definition for DropDownListFor and no extension method blah blah.Medley
That's because you didn't brought the necessary namespace in which this extension method is defined into scope : using System.Web.Mvc.Html.Shanteshantee
great but how to add client side validation on the selectMatins
@Timeless, why would you validate a select list? You are providing the values, there is no user input to check. You should be preventing garbage data from being an option for the user to select.Semantic

© 2022 - 2024 — McMap. All rights reserved.