2011-10-29 16 views
6

pisałem wyrażenie HtmlHelper używam dużo czasu, aby umieścić tagi tytułowe w moich listach rozwijanych tak:Zabawa z LINQ zgłaszanie metod przedłużających

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

... to działa maśle, ale są chwile, kiedy chcę iść dalej. Chciałbym dostosować identyfikator, wyświetlacz i tytuły tej bestii bez konieczności wypisywania html. Na przykład, jeśli mam kilka klas w modelu tak:

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

Moim zdaniem mogę napisać:

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

... i będę miał piękny rozwijaną z atrybutów tytuł etc To jest świetne, o ile wymienione elementy mają właściwości dokładnie tak, jak chciałbym je wyświetlić. Ale to, co naprawdę chciałbym zrobić coś takiego jak:

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

... i mają w tym przypadku, atrybut tytuł na opcjach być połączeniem właściwości itemName a właściwość itemDescription. Wyznaję meta-poziom wyrażeń lambda, a funkcje Linq sprawiają, że mam trochę zawrotów głowy. Czy ktoś może wskazać mi właściwy kierunek?

wynik końcowy Dla tych, którzy są ciekawi, następujący kod daje mi pełną kontrolę nad listy Wybierz identyfikator, właściwości Title, a displayText przy użyciu wyrażeń lambda:

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

UWAGA: ToStringOrEmpty() Rozszerzenie to jest moja metoda, która przyjmuje obiekt i robi ToString() na nim, chyba że jest pusty, w takim przypadku przywraca pusty łańcuch. –

Odpowiedz

5

Jeśli nie potrzebne tytuł atrybut na poszczególnych opcji kod może zostać uproszczony do:

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

i używane tak:

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

A jeśli potrzebujesz atrybut title, cóż, trzeba będzie wdrożyć wszystko, że pomocnik DropDownList robi ręcznie, co może być dość uciążliwe. Oto przykład z tylko niewielką część wszystkich funkcjonalności:

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

a następnie używać tak:

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

Pierwsza podana opcja jest dokładnie tym, czego próbuję uniknąć. Chcę całkowitej kontroli nad kodem, nie chcę wysyłać go do kodu kogoś innego ... z podanego powodu, tracę zdolność robienia wielu fajnych rzeczy. Twój drugi pomysł ma wiele obietnic dotyczących tego, co próbuję zrobić ... Będę grał z tą koncepcją i zobaczę, czy uda mi się to osiągnąć. –

+0

@JeremyHolovacs, Naprawdę nie widzę, jakie * fajne rzeczy * tracisz zdolność do czynienia ze standardowymi pomocnikami ASP.NET MVC. Cóż, tak, nie możesz dodać 'title' do tagów' option', ale czy naprawdę jest to coś, czego potrzebujesz? Osobiście nie zawracałbym sobie głowy pisaniem takich pomocników. Po prostu zaprojektowałbym modele, które mają właściwość 'IEnumerable ' i wyrzuć ją do standardowego pomocnika 'Html.DropDownListFor'. Wykonuje zadanie. Jeśli nie chcesz używać modeli widoku i próbujesz przekazać modele domeny do widoku i próbujesz pisać niestandardowych pomocników, cóż, to kolejne pytanie. –

+0

To absolutnie.Większość moich wymagań dotyczących projektu wymaga nazwy w menu rozwijanym, ale opis w dymku. Co więcej, nie podoba mi się brak opcji *. Dlaczego warto się ustatkować, kiedy nie musisz? –