I have a enum that holds countries:
public enum CountryEnum
{
[Display(Name = "AF", ResourceType = typeof(Global))]
AF,
[Display(Name = "AL", ResourceType = typeof(Global))]
AL,
[Display(Name = "DZ", ResourceType = typeof(Global))]
DZ,
};
As you can see I make use of the DataAnnotations to localise the values.
Now I want to display a dropdownlist with all of the localised country names. I come up with this code:
public static string GetDisplayName<TEnum>(TEnum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DisplayAttribute[] attributes =
(DisplayAttribute[])fi.GetCustomAttributes(
typeof(DisplayAttribute), false);
if ((attributes != null) && (attributes.Length > 0))
return attributes[0].Name;
else
return value.ToString();
}
I have a Html helper that makes use of the above method:
public static MvcHtmlString EnumDropDownListFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression, 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 = GetDisplayName(value),
Value = value.ToString(),
Selected = value.Equals(metadata.Model)
};
// If the enum is nullable, add an 'empty' item to the collection
if (metadata.IsNullableValueType)
items = SingleEmptyItem.Concat(items);
return htmlHelper.DropDownListFor(expression, items, htmlAttributes);
}
The DropDown renders correctly, however, GetDisplayName doesn't return the localised value, it just display the name attribute (e.g. AF for the first entry).
How can I modify GetDisplayName method to return the localised value?
You need to update your GetDisplayName method to use the GetName() method rather than the Name property of the DisplayAttribute.
Like this:
public static string GetDisplayName<TEnum>(TEnum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DisplayAttribute[] attributes = (DisplayAttribute[])fi.
GetCustomAttributes(typeof(DisplayAttribute), false);
if ((attributes != null) && (attributes.Length > 0))
return attributes[0].GetName();
else
return value.ToString();
}
From the MSDN documentation for DisplayAttribute.Name:
Do not use this property to get the value of the Name property. Use
the GetName method instead.
The MSDN documentation for the GetName() method has this to say:
Returns the localized string for the Name property, if the
ResourceType property has been specified and the Name property
represents a resource key; otherwise, the non-localized value of the
Name property.
I was having a similar problem, having setup the Display Attribute as you did (easier since classes use the same attribute to load localizations), so took your initial code and tweaked it a bit, now it is displaying the localization string as expected. I don't think it is the most clever or optimized way of doing it, but it works. Hopefully that's what you intended.
public static string GetDisplayName<TEnum>(TEnum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DisplayAttribute[] attributes = (DisplayAttribute[])fi.
GetCustomAttributes(typeof(DisplayAttribute), false);
if ((attributes != null) && (attributes.Length > 0))
{
string key = attributes[0].Name;
string localizedString = attributes[0].ResourceType.GetProperty(key).GetValue("").ToString();
return localizedString;
}
else
return value.ToString();
}
Related
I'm just learning visual c# in visual studio
Ok, so I have a ton of data fields in a form, and then I want to write the handlers to all call one main method, update, which then updates a resultsEntry object, which encapsulates a bunch of uint variables with various names.
How do I write an update method to put in either the results object, or the update method that will take the name of the variable in resultsEntry as a string, and the integer to update it with, and then update that field.
Basically, I need to do a
resultsEntry.(inStringHere) = inValueHere;
where resultsEntry is the object being updated, the inStringHere specifies the field to be updated, and the inValueHere represents the integer value to assign to it.
Thanks!
Sam French
You have two challenges,
Setting a field/property in class using a string (the focus of your question). This will be accomplished using reflection.
Converting values to the type in your class (this may not be a problem for you, you may have 'typed' values. I have an ugly solution because this is not the main focus of your question.
Setting a property by name (see comments preceded with '**'):
static class Program
{
// A 'ResultEntry' type
public class ResultEntry
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
static void Main()
{
// some tuples Field1 = Property Name; Field2 = Raw Value (string)
List<Tuple<string, string>> rawEntries = new List<Tuple<string, string>>() {
new Tuple<string,string>("ID", "1")
, new Tuple<string, string>("FirstName", "George")
, new Tuple<string, string>("LastName", "Washington")
};
ResultEntry resultEntry = new ResultEntry();
// ** Get MemberInfo's for your ResultEntry. Do this once, not for each instance of ResultEntry!
MemberInfo[] members = resultEntry.GetType().GetMembers(BindingFlags.Public | BindingFlags.Instance);
// Iterate over input
foreach (var raw in rawEntries)
{
// find a MemberInfo (PropertyInfo) that matches your input 'PropertyName'
MemberInfo member = members.FirstOrDefault(m => m.MemberType == MemberTypes.Property && m.Name == raw.Item1);
if (member != null)
{
// if you input is typed you will not have to deal with
// conversion of the string to the actual type of the property
object val = raw.Item2.MyConverter(((PropertyInfo)member).PropertyType);
// ** set the value in 'ResultEntry'
((PropertyInfo)member).SetValue(resultEntry, val, null);
}
}
Console.WriteLine(string.Format("Result Entry: ID = {0}, FirstName = {1}, LastName = {2}", resultEntry.ID, resultEntry.FirstName, resultEntry.LastName));
Console.ReadLine();
}
}
If you need to deal with converting raw string input to type (e.g. string to int), then is just one strategy...
public static class Extensions
{
public static object MyConverter(this string rawValue, Type convertToMe)
{
// ugly, there are other strategies
object val;
if (convertToMe == typeof(Int32))
val = Convert.ToInt32(rawValue);
// ... add other type conversions
else
val = rawValue;
return val;
}
}
Hope this helps.
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);
}
}
}
MVC3, Entity Framework 4.1 Code first.
Working with 2 tables
Model:
public class UniversityMaster
{
[Key]
public string UniversityId { get; set; }
public string UniversityName { get; set; }
}
public class ProgramMaster
{
[Key]
public string ProgramId { get; set; }
public string ProgramName { get; set; }
public string UniversityId { get; set; }
public virtual UniversityMaster University { get; set; } // navigation property
}
Dynamic expression for sorting (just to avoid a switch case statement):
public virtual IQueryable< ProgramMaster > GetQueryableSort(string sortField="", string sortDirection="")
{
IQueryable<ProgramMaster> query = _dbSet;
ParameterExpression pe = Expression.Parameter(typeof(ProgramMaster), string.Empty);
MemberExpression property = Expression.PropertyOrField(pe, sortField);
//get a exception here if the sort field is of navigation property (University.UniversityName)
LambdaExpression lambda = Expression.Lambda(property, pe);
if (sortDirection == "ASC")
orderbydir = "OrderBy";
else
orderbydir = "OrderByDescending";
MethodCallExpression call = Expression.Call(typeof(Queryable),
orderbydir, new Type[] { typeof(TEntity), property.Type }, query.Expression, Expression.Quote(lambda));
var returnquery = (IOrderedQueryable<ProgramMaster>)query.Provider.CreateQuery< ProgramMaster >(call);
return returnquery;
}
The page is displaying a grid with two columns Program Name and University Name using webgrid. The sorting work fine for Program Name column, however fails if sorted by University Name as this property is in UniversityMaster and the Expression.PropertyOrField searches this property in ProgramMaster. Here is the exception:
University.UniversityName' is not a member of type 'App.Core.Model.ProgramMaster
My question is how I make this work for navigation properties of my model class.
Hope I was able explain the scenario. Any help is appreciated.
Well that's because the MemberExpression is trying to call a member named Univerty.UniversityName on the parameter. What you want to do is call a member named Univerity on the parameter, then call UniversityName on that. Effectively, you need to iteratively resolve the property names.
public virtual IQueryable< ProgramMaster > GetQueryableSort(string sortField = "", string sortDirection = "")
{
IQueryable<ProgramMaster> query = _dbSet;
var propertyNames = sortField.Split(".");
ParameterExpression pe = Expression.Parameter(typeof(ProgramMaster), string.Empty);
Expression property = pe;
foreach(var prop in propertyName)
{
property = Expression.PropertyOrField(property, prop);
}
LambdaExpression lambda = Expression.Lambda(property, pe);
if (sortDirection == "ASC")
orderbydir = "OrderBy";
else
orderbydir = "OrderByDescending";
MethodCallExpression call = Expression.Call(
typeof(Queryable),
orderbydir,
new Type[] { typeof(TEntity), property.Type },
query.Expression,
Expression.Quote(lambda));
var returnquery = (IOrderedQueryable<ProgramMaster>)query.Provider.CreateQuery<ProgramMaster>(call);
return returnquery;
}
Microsoft has a DynamicQueryable class which can be used to dynamically construct certain portions of a LINQ query using strings. With this you can say myQuery.OrderBy("University.UniversityName") and it will handle building the expression. The same library also supports dynamic construction of SELECT and WHERE clauses.
You can find a copy of the source as part of the excellent EntityFramework.Extended package by Loresoft. Microsoft's file is at https://github.com/loresoft/EntityFramework.Extended/blob/master/Source/EntityFramework.Extended/Dynamic/DynamicQueryable.cs
I know there is several threads on this topic. I see a lot of solutions but most of them demands the knowledge of which type the enum is. Since Enum by default renders uses the template for string, it's more difficult to find out the exact type.
Has someone got a complete example on how to solve this?
Edit. Based on Roys suggestion, here's my code.
HtmlHelpers.cs
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
var type = htmlHelper.ViewData.ModelMetadata.ModelType;
var inputName = type.Name;
var value = htmlHelper.ViewData.Model == null ? default(TProperty) : expression.Compile()(htmlHelper.ViewData.Model);
var selected = value == null ? String.Empty : value.ToString();
return htmlHelper.DropDownList(inputName, ToSelectList(type, selected));
}
private static IEnumerable<SelectListItem> ToSelectList(Type enumType, string selectedItem)
{
var items = new List<SelectListItem>();
foreach (var item in Enum.GetValues(enumType))
{
var fi = enumType.GetField(item.ToString());
var attribute = fi.GetCustomAttributes(typeof(DescriptionAttribute), true).FirstOrDefault();
var title = attribute == null ? item.ToString() : ((DescriptionAttribute)attribute).Description;
var listItem = new SelectListItem
{
Value = ((int)item).ToString(),
Text = title,
Selected = selectedItem == ((int)item).ToString()
};
items.Add(listItem);
}
return new SelectList(items, "Value", "Text");
}
Viewmodel
public class AddTestForm
{
[UIHint("Enum")]
public EnumType Type { get; set; }
public string Description { get; set; }
public enum EnumType
{
One = 1,
Two = 2
}
EditorTemplates/Enum.cshtml
#model object
#Html.EnumDropDownListFor(x => x)
And in views...
#Html.EditorForModel()
The problem now is that I can't get the enum to fill after the form has been posted.
Your helper generates wrong names to the generated select elements:
<select id="Type_EnumType" name="Type.EnumType">
The DefaultModelBinder uses the name attribute to match the property names. Your helper generated name "Type.EnumType" doesn't match with the Model property name "Type". To make it work instead of the Type name you need to get the property name from the expression. It's quite easy with the ExpressionHelper class:
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression) where TModel : class
{
...
return htmlHelper.DropDownList(ExpressionHelper.GetExpressionText(expression), ToSelectList(type, selected));
}
I want to retrieve a specific record using IQueryable. But i get error 'No generic method 'Where' on type 'System.Linq.Queryable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.'. I got the selected row id, but I cannot display it out. Here is my code.
internal static IQueryable GetRecordsFromPrimaryKeys(this IQueryable datasource, List<FilterDescriptor> primaryKeys)
{
IQueryable data = datasource;
ParameterExpression paramExp = null;
bool firstLoop = false;
System.Linq.Expressions.Expression predicate = null;
var RecordType = datasource.GetObjectType();
paramExp = RecordType.Parameter();
foreach (FilterDescriptor primaryKey in primaryKeys)
{
if (!(firstLoop))
{
predicate = data.Predicate(paramExp, primaryKey.ColumnName, primaryKey.Value, FilterType.Equals, false, RecordType);
firstLoop = true;
}
else
{
predicate = predicate.AndPredicate(data.Predicate(paramExp, primaryKey.ColumnName, primaryKey.Value, FilterType.Equals, false, RecordType));
}
}
if (paramExp != null && predicate != null)
{
var lambda = Expression.Lambda(predicate, paramExp);
data = data.Provider.CreateQuery(
Expression.Call(
typeof(Queryable),
"Where",
new Type[] { data.ElementType },
data.Expression,
lambda
)
);
}
return data;
}
My Code works well for IEnumerable/IQueryable/ICollection . But it throws the exception when i specify the class with the keyword virtual and type as ICollection. My code is
public class RoomType
{
public int ID { get; set; }
[MaxLength(10, ErrorMessage = "Room code cannot be longer than 10 characters.")]
public string Code { get; set; }
[MaxLength(50, ErrorMessage = "Room name cannot be longer than 50 characters.")]
public string Name { get; set; }
public virtual ICollection<RoomCategory> RoomCategories { get; set; }
}
Some random values gets appended to 'RecordType' while using the keyword 'virtual'. I think this leads to the exception. Still searching for the solution.
I don't know what is going wrong . Any suggestions welcome.
Thanks.
I just ran into a similar situation. The problem stems from the fact that in some cases you're dealing with the "proxy" not the actual entity. So, you want to make sure that RecordType matches data.ElementType.
try:
var recordType = datasource.GetObjectType();
// make sure we have the correct type (not the proxy)
if (recordType.BaseType.Name != "Object")
recordType = recordType.BaseType;
Or better yet, try:
var recordType = data.ElementType
Try to use typeof(Enumerable) instead of typeof(Queryable)