Fun (?) with Linq Expressions in extension methods - linq

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

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
)

Related

how can i have a CheckBoxFor using int?

I need help to build a CheckBoxFor for getting an int value.
Something like:
#Html.CheckBoxForInt(m => m.foo.intValue)
It should be checked if intValue = 1 else not checked.
Why don't you expose a bool property in your model that converts to/from the int?
Something like this:
public bool BoolValue
{
get { return IntValue == 1; }
set { IntValue = value ? 1 : 0;}
}
public int IntValue { get; set; }
Then you could use it to create the checkbox
#Html.CheckBoxFor(m => m.foo.BoolValue)
For some reason the response above gave me errors but based on the same idea I've change the code like this:
public int IntValue { get; set; }
public bool BoolValue
{
get { return IntValue == 1; }
set {
if(value)
IntValue = 1;
else
IntValue = 0;
}
}
and that work for me.
Here's the checkbox helper example for handling int values:
public static MvcHtmlString CheckBoxIntFor<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, int>> expression, object htmlAttributes)
{
// get the name of the property
string[] propertyNameParts = expression.Body.ToString().Split('.');
// create name and id for the control
string controlId = String.Join("_", propertyNameParts.Skip(1));
string controlName = String.Join(".", propertyNameParts.Skip(1));
// get the value of the property
Func<TModel, int> compiled = expression.Compile();
int booleanSbyte = compiled(html.ViewData.Model);
// convert it to a boolean
bool isChecked = booleanSbyte == 1;
// build input element
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("id", controlId);
checkbox.MergeAttribute("name", controlName);
checkbox.MergeAttribute("type", "checkbox");
if (isChecked)
{
checkbox.MergeAttribute("checked", "checked");
checkbox.MergeAttribute("value", "1");
}
else
{
checkbox.MergeAttribute("value", "0");
}
SetStyle(checkbox, htmlAttributes);
// script to handle changing selection
string script = "<script>" +
"$('#" + controlId + "').change(function () { " +
"if ($('#" + controlId + "').is(':checked')) "+
"$('#" + controlId + "').val('1'); " +
"else " +
"$('#" + controlId + "').val('0'); " +
"}); " +
"</script>";
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.SelfClosing) + script);
}
private static void SetStyle(TagBuilder control, object htmlAttributes)
{
if(htmlAttributes == null)
return;
// get htmlAttributes
Type t = htmlAttributes.GetType();
PropertyInfo classInfo = t.GetProperty("class");
PropertyInfo styleInfo = t.GetProperty("style");
string cssClasses = classInfo?.GetValue(htmlAttributes)?.ToString();
string style = styleInfo?.GetValue(htmlAttributes)?.ToString();
if (!string.IsNullOrEmpty(style))
control.MergeAttribute("style", style);
if (!string.IsNullOrEmpty(cssClasses))
control.AddCssClass(cssClasses);
}
A slightly different approach:
my database needs to accept an int, and i want to have a checkbox that sends that int from my form in Razor. on the page i also use angularJS (also easy to just use pure JS). So here is a solution that does not involve changing models etc.
in form:
<input ng-click="checkCheckBox()" type="checkbox" id="myCheck">
#Html.HiddenFor(m => m.Role, new { id = "id_checkbox", Value = 0 })
in javascript:
$scope.checkCheckBox= function(){
var x = document.getElementById("myCheck").checked;
if (x == true) { $('#id_checkbox').val(3);}
else { $('#id_checkbox').val(0);}
}

MVC 3 helper for radiobuttonlist enum

I need some help. I'm trying to build a view where I need groups of radiobuttons of enum types.
I have several enum types(classes) like this:
[DataContract(Namespace = Constants.SomeDataContractNamespace)]
public enum OneEnumDataContract
{
[Display(Name = "Text_None", Description = "Text_None", ResourceType = typeof(TextResource))]
[EnumMember]
None = 0,
[Display(Name = "Text_Medium", Description = "Text_Medium", ResourceType = typeof(TextResource))]
[EnumMember]
Medium = 1,
[Display(Name = "Text_Very", Description = "Text_Very", ResourceType = typeof(TextResource))]
[EnumMember]
Very = 2
}
In my model(a datacontract, using WCF) I have this property for the enum datacontract:
[DataMember(Order = 23)]
[Display(Name = "EnumValue", Description = "EnumValue_Description", ResourceType = typeof(TextResource))]
public OneEnumDataContract EnumClass1 { get; set; }
In my view I would try to make the group of radiobuttons like this(with a helper):
#Html.RadioButtonListEnum("EnumList1", Model.EnumClass1)
My helper:
public static MvcHtmlString RadioButtonListEnum<TModel>(this HtmlHelper<TModel> helper, string NameOfList, object RadioOptions)
{
StringBuilder sb = new StringBuilder();
//som other code for pairing with resourcefile...
foreach(var myOption in enumTexts.AllKeys)
{
sb.Append("<p>");
sb.Append(enumTexts.GetValues(myOption)[0]);
sb.Append(helper.RadioButton(NameOfList, System.Convert.ToInt16(myOption)));
sb.Append("</p>");
}
return MvcHtmlString.Create(sb.ToString());
}
This gives me the first enumvalue in OneEnumDataContract, None, as the parameter RadioOptions.
How can I get all the enumvalues in the datacontract into the helper?
This is one I created recently. It won't work if you try it on a non-enum but works for my enum needs. I copied bit's and pieces from different DropDownList helpers like nikeaa posted.
#region RadioButtonList
public static MvcHtmlString RadioButtonListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes = null) where TModel : class
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
String field = ExpressionHelper.GetExpressionText(expression);
String fieldname = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(field);
var inputName = fieldname;
TProperty val = GetValue(htmlHelper, expression);
var divTag = new TagBuilder("div");
divTag.MergeAttribute("id", inputName);
divTag.MergeAttribute("class", "radio");
foreach (var item in Enum.GetValues(val.GetType()))
{
DisplayAttribute[] attr = (DisplayAttribute[])item.GetType().GetField(item.ToString()).GetCustomAttributes(typeof(DisplayAttribute), true);
if (attr == null || attr.Length == 0 || attr[0].Name != null)
{
string name = attr != null && attr.Length > 0 && !string.IsNullOrWhiteSpace(attr[0].Name) ? attr[0].Name : item.ToString();
var itemval = item;
var radioButtonTag = RadioButton(htmlHelper, inputName, new SelectListItem { Text = name, Value = itemval.ToString(), Selected = val.Equals(itemval) }, htmlAttributes);
divTag.InnerHtml += radioButtonTag;
}
}
return new MvcHtmlString(divTag.ToString());
}
public static string RadioButton(this HtmlHelper htmlHelper, string name, SelectListItem listItem,
IDictionary<string, object> htmlAttributes)
{
var inputIdSb = new StringBuilder();
inputIdSb.Append(name)
.Append("_")
.Append(listItem.Value);
var sb = new StringBuilder();
var builder = new TagBuilder("input");
if (listItem.Selected) builder.MergeAttribute("checked", "checked");
builder.MergeAttribute("type", "radio");
builder.MergeAttribute("value", listItem.Value);
builder.MergeAttribute("id", inputIdSb.ToString());
builder.MergeAttribute("name", name);
builder.MergeAttributes(htmlAttributes);
sb.Append(builder.ToString(TagRenderMode.SelfClosing));
sb.Append(RadioButtonLabel(inputIdSb.ToString(), listItem.Text, htmlAttributes));
sb.Append("<br>");
return sb.ToString();
}
public static string RadioButtonLabel(string inputId, string displayText,
IDictionary<string, object> htmlAttributes)
{
var labelBuilder = new TagBuilder("label");
labelBuilder.MergeAttribute("for", inputId);
labelBuilder.MergeAttributes(htmlAttributes);
labelBuilder.InnerHtml = displayText;
return labelBuilder.ToString(TagRenderMode.Normal);
}
public static TProperty GetValue<TModel, TProperty>(HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
TModel model = htmlHelper.ViewData.Model;
if (model == null)
{
return default(TProperty);
}
Func<TModel, TProperty> func = expression.Compile();
return func(model);
}
#endregion
I use it like this
#Html.RadioButtonListFor(m => m.PlayFormat)
You may need to more code to set the correct element name for more complicated uses.
If the enum items have a Display attribute, the name is displayed. Otherwise the enum item is displayed. If the Display name is null, that value is not shown as an option. In this enum, "None" isn't displayed, "Singles" is displayed from the enum value, "Men's Doubles" and all the other's have text from [Display(Name="Men's Doubles")]
public enum PlayFormat
{
[Display(Name=null)]
None = 0,
Singles = 1,
[Display(Name = "Men's Doubles")]
MenDoubles = 2,
[Display(Name = "Women's Doubles")]
WomenDoubles = 3,
[Display(Name = "Mixed Doubles")]
MixedDoubles = 4,
[Display(Name = "Men's Group")]
MenGroup = 5,
[Display(Name = "Women's Group")]
WomenGroup = 6,
[Display(Name = "Mixed Group")]
MixedGroup = 7
}
The page looks like this (except each - is a radio button)
- Singles
- Men's Doubles
- Women's Doubles
- Mixed Doubles
- Men's Group
- Women's Group
- Mixed Group
Here is a helper method that I found on the internet to make a drop down list from an enum. You should be able to modify this code to create radio buttons instead of a drop down.
namespace Localicy.HtmlHelpers {
public static class HtmlHelperExtensions {
private static Type GetNonNullableModelType(ModelMetadata modelMetadata) {
Type realModelType = modelMetadata.ModelType;
Type underlyingType = Nullable.GetUnderlyingType(realModelType);
if (underlyingType != null)
realModelType = underlyingType;
return realModelType;
}
private static readonly SelectListItem[] SingleEmptyItem = new[] { new SelectListItem { Text = "", Value = "" } };
public static string GetEnumDescription<TEnum>(TEnum value) {
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if ((attributes != null) && (attributes.Length > 0))
return attributes[0].Description;
else
return value.ToString();
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression) {
return EnumDropDownListFor(htmlHelper, expression, null, null);
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, string defaultValueText) {
return EnumDropDownListFor(htmlHelper, expression, defaultValueText, null);
}
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, string defaultValueText, object htmlAttributes) {
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
Type enumType = GetNonNullableModelType(metadata);
IEnumerable<TEnum> values = Enum.GetValues(enumType).Cast<TEnum>();
IEnumerable<SelectListItem> items = from value in values
select new SelectListItem {
Text = GetEnumDescription(value),
Value = value.ToString(),
Selected = value.Equals(metadata.Model)
};
// If the enum is nullable, add an 'empty' item to the collection
if (metadata.IsNullableValueType || defaultValueText != null)
if(defaultValueText != null) {
SelectListItem[] tempItem = new[] { new SelectListItem { Text = defaultValueText, Value = "" } };
items = tempItem.Concat(items);
}
else
items = SingleEmptyItem.Concat(items);
//items = (new ).Concat(items)
return htmlHelper.DropDownListFor(expression, items, htmlAttributes);
}
}
}

MultiSelectList does not highlight items in the selectedValues list

In a ASP.NET MVC (Razor) project, I'm using a ListBox with Multi Select option in a Edit View, there was a problem in highlighting the previously selected items by using selectedValues in MultiSelectList, so I asked a question on SO previously. According to the answers given for that question I decided to use a ViewModel (with AutoMapper) for passing the data to the View, without using the ViewBag, but still I have the same problem.. It does not select the items given in the selectedValues list
this is my new code
MODELS
public class Post
{
public int Id { get; set; }
...
public string Tags { get; set; }
}
public class PostEditViewModel
{
private DocLibraryContext db = new DocLibraryContext();
public int Id { get; set; }
..
public MultiSelectList TagsList { get; set; }
}
Controller
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
PostEditViewModel postEditViewModel = Mapper.Map<Post, PostEditViewModel>(post);
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
postEditViewModel.TagsList = new MultiSelectList(tagsQuery, "Id", "Name", post.Tags.Split(','));
return View(postEditViewModel);
}
VIEW
<div class="editor-field">
#Html.ListBoxFor(model => model.Tags, Model.TagsList as MultiSelectList)
</div>
What am I doing wrong here? Please help....
UPDATE 1 :
changed controller to
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
PostEditViewModel postEditViewModel = Mapper.Map<Post, PostEditViewModel>(post);
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
var selectedIds = post.Tags.Split(',').Select(n => tagsQuery.First(t => t.Name == n));
postEditViewModel.TagsList = new MultiSelectList(tagsQuery, "Id", "Name", selectedIds);
return View(postEditViewModel);
}
but I get the same results.
UPDATE 2:
I tried changing code (as in this tutorial), which worked, But I need to use previous method..
MODELS
public Post Post { get; set; }
public MultiSelectList TagsList { get; set; }
public PostEditViewModel(Post post)
{
Post = post;
var tagsQuery = from d in db.Tags
orderby d.Name
select d;
TagsList = new MultiSelectList(tagsQuery, "Name", "Name", post.Tags.Split(','));
}
Controller
public ActionResult Edit(int id)
{
Post post = db.Posts.Find(id);
return View(new PostEditViewModel(post));
}
VIEW
<div class="editor-field">
#Html.ListBox("Tags", Model.TagsList as MultiSelectList)
</div>
What makes the difference...??
The problem is with the construction of your MultiSelectList:
new MultiSelectList(tagsQuery, "Id", "Name", post.Tags.Split(','));
You are specifying that the values for the elements will be taken from each tag's Id property, but then for the actual selected values you are passing in an array of strings which presumably corresponds to the Names of the tags. It doesn't matter that you also specify Name to be the property from which the display text will be determined; the selectedValues parameter matches against values, not display text.
To fix this, project each tag name into its corresponding Id:
var selectedIds = post.Tags.Split(',').Select(n => tagsQuery.First(t => t.Name == n).Id);
new MultiSelectList(tagsQuery, "Id", "Name", selectedIds);
Update:
Oops, there was a mistake in the code above.
I edited the answer to add a required .Id at the end of the selectedIds initialization -- the previous version was selecting tags, not ids (and of course they were comparing unequal, apples and oranges).
I had the same problem, I used my own extention method to generate the html and problem solved
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
object htmlAttributes)
{
return ListBoxMultiSelectFor(helper, expression, selectList, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
IDictionary<string, object> htmlAttributes)
{
string name = ExpressionHelper.GetExpressionText(expression);
TagBuilder selectTag = new TagBuilder("select");
selectTag.MergeAttributes(htmlAttributes);
selectTag.MergeAttribute("id", name, true);
selectTag.MergeAttribute("name", name, true);
foreach (SelectListItem item in selectList)
{
TagBuilder optionTag = new TagBuilder("option");
optionTag.MergeAttribute("value", item.Value);
if (item.Selected) optionTag.MergeAttribute("selected", "selected");
optionTag.InnerHtml = item.Text;
selectTag.InnerHtml += optionTag.ToString();
}
return new MvcHtmlString(selectTag.ToString());
}

Localized enum strings in SelectList

In my MVC3 app. I'm using select list to populate combo box with enum values like this
<div class="editor-field">
#Html.DropDownListFor(x => x.AdType, new SelectList(Enum.GetValues(typeof(MyDomain.Property.AdTypeEnum))), " ", new { #class = "dxeButtonEdit_Glass" })
</div>
MyDomain.Property looks like this
public enum AdTypeEnum
{
Sale = 1,
Rent = 2,
SaleOrRent = 3
};
How can I use my localized strings to localize these enums?
You could write a custom attribute:
public class LocalizedNameAttribute: Attribute
{
private readonly Type _resourceType;
private readonly string _resourceKey;
public LocalizedNameAttribute(string resourceKey, Type resourceType)
{
_resourceType = resourceType;
_resourceKey = resourceKey;
DisplayName = (string)_resourceType
.GetProperty(_resourceKey, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
.GetValue(null, null);
}
public string DisplayName { get; private set; }
}
and a custom DropDownListForEnum helper:
public static class DropDownListExtensions
{
public static IHtmlString DropDownListForEnum<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
string optionLabel,
object htmlAttributes
)
{
if (!typeof(TProperty).IsEnum)
{
throw new Exception("This helper can be used only with enum types");
}
var enumType = typeof(TProperty);
var fields = enumType.GetFields(
BindingFlags.Static | BindingFlags.GetField | BindingFlags.Public
);
var values = Enum.GetValues(enumType).OfType<TProperty>();
var items =
from value in values
from field in fields
let descriptionAttribute = field
.GetCustomAttributes(
typeof(LocalizedNameAttribute), true
)
.OfType<LocalizedNameAttribute>()
.FirstOrDefault()
let displayName = (descriptionAttribute != null)
? descriptionAttribute.DisplayName
: value.ToString()
where value.ToString() == field.Name
select new { Id = value, Name = displayName };
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var enumObj = metadata;
var selectList = new SelectList(items, "Id", "Name", metadata.Model);
return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes);
}
}
And then it's easy:
Model:
public enum AdTypeEnum
{
[LocalizedName("Sale", typeof(Messages))]
Sale = 1,
[LocalizedName("Rent", typeof(Messages))]
Rent = 2,
[LocalizedName("SaleOrRent", typeof(Messages))]
SaleOrRent = 3
}
public class MyViewModel
{
public AdTypeEnum AdType { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
AdType = AdTypeEnum.SaleOrRent
});
}
}
View:
#model MyViewModel
#Html.DropDownListForEnum(
x => x.AdType,
" ",
new { #class = "foo" }
)
Finally you should create a Messages.resx file which will contain the necessary keys (Sale, Rent and SaleOrRent). And if you want to localize you simply define a Messages.xx-XX.resx with the same keys for a different culture and swap the current thread culture.

Best way to sort a DropDownList in MVC3 / Razor using helper method

Hi so I'm pretty new to MVC3 and Razor and I've been trying to get my head around it the past few days. I've been given a task by my project architect to create a helper method that sorts a drop down list in an MVC View. I have a View that retrieves various data from a Controller and I'm returning some values that I want to appear in a drop down list. I've been told not to sort it in the Controller and also to pass the field that we want to sort by into the helper method. I could do it like below but the architect wants to keep the view free of c sharp code:
#Html.DropDownListFor(model => model.StudyName, new SelectList(ViewBag.StudyTypes, "Value", "Text").OrderBy(l => l.Text))
So I've created some sample code and some extension methods to try and get it to work. My idea is to replicate the existing Html.DropDownList method and allow the passing of 'object htmlAttributes' so I can set the style as part of the method call.
Here's my code so far. I'm returning the data for the drop down in ViewBag.StudyTypes in the Edit Controller method:
public ActionResult Edit(int id)
{
IEnumerable<SelectListItem> mySelectList = new List<SelectListItem>();
IList<SelectListItem> myList = new List<SelectListItem>();
for (int i = 0; i < 5; i++)
{
myList.Add(new SelectListItem()
{ Value = i.ToString(), Text = "My Item " + i.ToString(), Selected = i == 2 }
);
}
mySelectList = myList;
ViewBag.StudyTypes = mySelectList;
StudyDefinition studydefinition = db.StudyDefinitions.Find(id);
return View(studydefinition);
}
Here's my View code:
#model MyStudyWeb.Models.StudyDefinition
#using MyStudyWeb.Helpers
#{
ViewBag.Mode = "Edit";
}
<div>
#Html.DropDownListSorted(new SelectList(ViewBag.StudyTypes, "Value", "Text"))<br />
#Html.DropDownListSorted("MyList", new SelectList(ViewBag.StudyTypes, "Value", "Text"))<br />
</div>
Finally below are the extension methods I'm trying to get to work. The first extension method does nothing, I just get a blank space at that point in the View. The second method kind of works but it's ugly. For the 3rd method I don't know how to specify an 'order by' parameter as the OrderBy on an IEnumerable expects a Linq expression.
namespace StudyDefinition.Helpers
{
public static class HtmlHelperExtensions
{
// 1st sort method: sort the passed in list and return a new sorted list
public static SelectList DropDownListSorted(this HtmlHelper helper, IEnumerable<SelectListItem> selectList)
{
var x = new SelectList(selectList.ToList()).OrderBy(l => l.Text);
return x as SelectList;
}
// 2nd sort method: return IHtml string and create <select> list manually
public static IHtmlString DropDownListSorted(this HtmlHelper helper, string name, SelectList selectList)
{
StringBuilder output = new StringBuilder();
(selectList).OrderBy(l => l.Text);
output.Append("<select id=" + name + " name=" + name + ">");
foreach (var item in selectList)
{
output.Append("<option value=" + item.Value.ToString() + ">" + item.Text + "</option>");
}
output.Append("</select>");
return MvcHtmlString.Create(output.ToString());
}
// 3rd sort method: pass in order by parameter - how do I use this?
public static IHtmlString DropDownListSorted(this HtmlHelper helper, string name, SelectList selectList, string orderBy)
{
StringBuilder output = new StringBuilder();
//How do I use the orderBy parameter?
(selectList).OrderBy(l => l.Text);
output.Append("<select id=" + name + " name=" + name + ">");
foreach (var item in selectList)
{
output.Append("<option value=" + item.Value.ToString() + ">" + item.Text + "</option>");
}
output.Append("</select>");
return MvcHtmlString.Create(output.ToString());
}
}
}
I really don't know the best approach to take, there may be a much simpler way that I'm totally missing and I might be at the point where I can't see the wood for the trees anymore. Some questions
Should I return a SelectList or an MvcHtmlString, or something else entirely?
For the first extension method how do I get the returned SelectList to render in the View?
How to I pass in a parameter to my extension methods that specifies the sort order?
How do I pass an 'object htmlAttributes' parameter, and how do I apply this object / parameter to the SelectList?
If anyone has some ideas or suggestions then I'd appreciate some feedback :)
The first and most important part of your code would be to get rid of any ViewBag/ViewData (which I personally consider as cancer for MVC applications) and use view models and strongly typed views.
So let's start by defining a view model which would represent the data our view will be working with (a dropdownlistg in this example):
public class MyViewModel
{
public string SelectedItem { get; set; }
public IEnumerable<SelectListItem> Items { get; set; }
}
then we could have a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel
{
// I am explicitly putting some items out of order
Items = new[]
{
new SelectListItem { Value = "5", Text = "Item 5" },
new SelectListItem { Value = "1", Text = "Item 1" },
new SelectListItem { Value = "3", Text = "Item 3" },
new SelectListItem { Value = "4", Text = "Item 4" },
}
};
return View(model);
}
}
and a view:
#model MyViewModel
#Html.DropDownListForSorted(
x => x.SelectedItem,
Model.Items,
new { #class = "foo" }
)
and finally the last piece is the helper method which will sort the dropdown by value (you could adapt it to sort by text):
public static class HtmlExtensions
{
public static IHtmlString DropDownListForSorted<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> items,
object htmlAttributes
)
{
var model = helper.ViewData.Model;
var orderedItems = items.OrderBy(x => x.Value);
return helper.DropDownListFor(
expression,
new SelectList(orderedItems, "Value", "Text"),
htmlAttributes
);
}
}
Just add in the sorting before you return the items to the dropdown list.
Do this:
Models: StudyViewModel.cs
public class StudyViewModel {
public string StudyName { get; set; }
public string StudyTypes { get; set; }
}
Controller: StudyController.cs
using System.Web.Mvc;
public class StudyController
{
public List<SelectListItem> studyTypes()
{
List<SelectListItem> itemList = new List<SelectListItem>();
for (var i=0; i<5; i++)
{
itemList.Add = new SelectListItem({
Value = i.ToString();
Text = "My Item";
});
}
// You can sort here....
List<SelectListItem> sortedList = itemList.OrderBy(x=>x.Text);
return sortedList;
}
public ActionResult Edit(int id)
{
//You won't need this because you get it using your
//controller's routine, instead
//ViewBag.StudyTypes = studySlots.OrderBy(e => e.Value);
//-- unless you need to add these values to the model for
// some reason (outside of filling the ddl), in which case....
// StudyViewModel svm = new StudyViewModel();
// svm.StudyTypes = studySlots.OrderBy(e => e.Value);
// svm.StudyName = "My Item";
// return View(svm);
// Otherwise, just....
return View();
}
}
View: Edit.cshtml
#Html.DropDownListFor(model => model.StudyName)
.OptionLabel('Select...')
.DataTextField('Text')
.DataValueField('Value')
.Datasource(source =>
{
// This is where you populate your data from the controller
source.Read(read =>
{
read.Action("studyTypes", "Study");
});
})
.Value(Model.StudyName != null ? Model.StudyName.ToString() : "")
)
This way will avoid ViewBags and just use a function to fill in the values, directly.
If you are using a database you can use a query to define the sort element
using (BDMMContext dataContext = new BDMMContext())
{
foreach (Arquiteto arq in dataContext.Arquitetos.SqlQuery("SELECT * FROM Arquitetos ORDER BY Nome"))
{
SelectListItem selectItem = new SelectListItem { Text = arq.Nome, Value = arq.Arquiteto_Id.ToString() };
//
list.Add(selectItem);
}
}

Resources