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.
Related
I'm trying find all items in my database that have at least one value in an array that matches any value in an array that I have in my code (the intersection of the two arrays should not be empty).
Basically, I'm trying to achieve this :
public List<Book> ListBooks(string partitionKey, List<string> categories)
{
return _client.CreateDocumentQuery<Book>(GetCollectionUri(), new FeedOptions
{
PartitionKey = new PartitionKey(partitionKey)
})
.Where(b => b.Categories.Any(c => categories.Contains(c))
.ToList();
}
With the Book class looking like this :
public class Book
{
public string id {get;set;}
public string Title {get;set;}
public string AuthorName {get;set;}
public List<string> Categories {get;set;}
}
However the SDK throws an exception saying that Method 'Any' is not supported when executing this code.
This doesn't work either :
return _client.CreateDocumentQuery<Book>(GetCollectionUri(), new FeedOptions
{
PartitionKey = new PartitionKey(partitionKey)
})
.Where(b => categories.Any(c => b.Categories.Contains(c))
.ToList();
The following code works because there's only one category to find :
public List<Book> ListBooksAsync(string category)
{
return _client.CreateDocumentQuery<Book>(GetCollectionUri())
.Where(b => b.Categories.Contains(category))
.ToList();
}
In plain SQL, I can queue multiple ARRAY_CONTAINS with several OR the query executes correctly.
SELECT * FROM root
WHERE ARRAY_CONTAINS(root["Categories"], 'Humor')
OR ARRAY_CONTAINS(root["Categories"], 'Fantasy')
OR ARRAY_CONTAINS(root["Categories"], 'Legend')
I'm trying to find the best way to achieve this with LINQ, but I'm not even sure it's possible.
In this situation I've used a helper method to combine expressions in a way that evaluates to SQL like in your final example. The helper method 'MakeOrExpression' below lets you pass a number of predicates (in your case the individual checks for b.Categories.Contains(category)) and produces a single expression you can put in the argument to .Where(expression) on your document query.
class Program
{
private class Book
{
public string id { get; set; }
public string Title { get; set; }
public string AuthorName { get; set; }
public List<string> Categories { get; set; }
}
static void Main(string[] args)
{
var comparison = new[] { "a", "b", "c" };
var target = new Book[] {
new Book { id = "book1", Categories = new List<string> { "b", "z" } },
new Book { id = "book2", Categories = new List<string> { "s", "t" } },
new Book { id = "book3", Categories = new List<string> { "z", "a" } } };
var results = target.AsQueryable()
.Where(MakeOrExpression(comparison.Select(x => (Expression<Func<Book, bool>>)(y => y.Categories.Contains(x))).ToArray()));
foreach (var result in results)
{
// Should be book1 and book3
Console.WriteLine(result.id);
}
Console.ReadLine();
}
private static Expression<Func<T,bool>> MakeOrExpression<T>(params Expression<Func<T,bool>>[] inputExpressions)
{
var combinedExpression = inputExpressions.Skip(1).Aggregate(
inputExpressions[0].Body,
(agg, x) => Expression.OrElse(agg, x.Body));
var parameterExpression = Expression.Parameter(typeof(T));
var replaceParameterVisitor = new ReplaceParameterVisitor(parameterExpression,
Enumerable.SelectMany(inputExpressions, ((Expression<Func<T, bool>> x) => x.Parameters)));
var mergedExpression = replaceParameterVisitor.Visit(combinedExpression);
var result = Expression.Lambda<Func<T, bool>>(mergedExpression, parameterExpression);
return result;
}
private class ReplaceParameterVisitor : ExpressionVisitor
{
private readonly IEnumerable<ParameterExpression> targetParameterExpressions;
private readonly ParameterExpression parameterExpression;
public ReplaceParameterVisitor(ParameterExpression parameterExpressionParam, IEnumerable<ParameterExpression> targetParameterExpressionsParam)
{
this.parameterExpression = parameterExpressionParam;
this.targetParameterExpressions = targetParameterExpressionsParam;
}
public override Expression Visit(Expression node)
=> targetParameterExpressions.Contains(node) ? this.parameterExpression : base.Visit(node);
}
}
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);
}
}
}
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());
}
Relatively new to MVC and trying to get a cascading dropdown list working for train times.
After looking at a lot of posts, people say that you should stay away from ViewBag/ViewData and instead focus on ViewModels, but I just can't seem to get my head round it, and it's driving me up the wall. Any tutorial seems to be either to complex or too easy and the whole viewModel idea just hasn't clicked with me yet.
So here is my scenario: I have an admin system where staff can add individual train journeys. For each train time, I have an input form where the user can choose a Company, and from there, I'd like the dropdownlist underneath to populate with a list of journey numbers, which indicate routes. Once they have chosen a number, they can carry on with the rest of the form, which is quite large, including time of travel, facilities on the train etc.
I've created a viewmodel like so:
public class JourneyNumbersViewModel
{
private List<SelectListItem> _operators = new List<SelectListItem>();
private List<SelectListItem> _journeys= new List<SelectListItem>();
[Required(ErrorMessage = "Please select an operator")]
public string SelectedOperator { get; set; }
[Required(ErrorMessage = "Please select a journey")]
public string SelectedJourney { get; set; }
public List<SelectListItem> Journeys
{
get { return _journeys; }
}
public List<SelectListItem> Operators
{
get
{
foreach(Operator a in Planner.Repository.OperatorRepository.GetOperatorList())
{
_operators.Add(new SelectListItem() { Text = a.OperatorName, Value = a.OperatorID.ToString() });
}
return _operators;
}
}
}
In my controller, I have this for the Create view:
public ActionResult Create()
{
return View(new JourneyNumbersViewModel());
}
And this is where it isn't really working for me - if I change my model at the top of the Create view to: #model Planner.ViewModels.JourneyNumbersViewModel, then the rest of my form throws errors as the model is no longer correct for the rest of the form. Is this the way it is supposed to work - what if you need to reference multiple view models with a single view?
I know this is a simple thing and as soon as it clicks I'll wonder how on earth I could have struggled with it in the first place, but if anyone can point out where I'm going wrong, I'd be very grateful.
I have done something similar. Here is some of the code (apologies upfront for this being quite long, but I wanted to make sure you could re-create this on your side):
View looks like this:
using Cascading.Models
#model CascadingModel
#{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Cascading Forms</h2>
<table>
#using(Html.BeginForm("Index", "Home"))
{
<tr>
<td>#Html.LabelFor(m=>m.CategoryId)</td>
<td>#Html.DropDownListFor(m => m.CategoryId, new SelectList(Model.Categories, "Id", "Name"), string.Empty)</td>
</tr>
<tr>
<td>#Html.LabelFor(m=>m.ProductId)</td>
<td>#Html.CascadingDropDownListFor(m => m.ProductId, new SelectList(Model.Products, "Id", "Name"), string.Empty, null, "CategoryId", "Home/CategorySelected")</td>
</tr>
<tr>
<td> </td>
<td><input type="submit" value="Go"/></td>
</tr>
}
</table>
the Model looks as follows:
public class CascadingModel
{
public int CategoryId { get; set; }
public List<Category> Categories { get; set; }
public int ProductId { get; set; }
public List<Product> Products { get; set; }
}
the real "clever" part of the system is the Html.CascadingDropDownListFor which looks as follows:
public static class MvcHtmlExtensions
{
public static MvcHtmlString CascadingDropDownListFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
string optionLabel,
IDictionary<string, Object> htmlAttributes,
string parentControlName,
string childListUrl
)
{
var memberName = GetMemberInfo(expression).Member.Name;
MvcHtmlString returnHtml = Html.SelectExtensions.DropDownListFor(htmlHelper, expression, selectList, optionLabel, htmlAttributes);
var returnString = MvcHtmlString.Create(returnHtml.ToString() +
#"<script type=""text/javascript"">
$(document).ready(function () {
$(""#<<parentControlName>>"").change(function () {
var postData = { <<parentControlName>>: $(""#<<parentControlName>>"").val() };
$.post('<<childListUrl>>', postData, function (data) {
var options = """";
$.each(data, function (index) {
options += ""<option value='"" + data[index].Id + ""'>"" + data[index].Name + ""</option>"";
});
$(""#<<memberName>>"").html(options);
})
.error(function (jqXHR, textStatus, errorThrown) { alert(jqXHR.responseText); });
});
});
</script>"
.Replace("<<parentControlName>>", parentControlName)
.Replace("<<childListUrl>>", childListUrl)
.Replace("<<memberName>>", memberName));
return returnString;
}
private static MemberExpression GetMemberInfo(Expression method)
{
LambdaExpression lambda = method as LambdaExpression;
if (lambda == null)
throw new ArgumentNullException("method");
MemberExpression memberExpr = null;
if (lambda.Body.NodeType == ExpressionType.Convert)
{
memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpr = lambda.Body as MemberExpression;
}
if (memberExpr == null)
throw new ArgumentException("method");
return memberExpr;
}
}
Controller Logic for those looking for it:
public ActionResult CategoriesAndProducts()
{
var viewModel = new CategoriesAndProductsViewModel();
viewModel.Categories = FetchCategoriesFromDataBase();
viewModel.Products = FetchProductsFromDataBase();
viewModel.CategoryId = viewModel.Categories[0].CategoryId;
viewModel.ProductId = viewModel.Products.Where(p => p.CategoryId).FirstOrDefault().ProductId;
return View(viewModel);
}
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
)