In an ASP.NET MVC3 application, if I wanted to model bind my form post data to an ExpandoObject (or my own object derived from DynamicObject where I implement my own Try... members) would I need to write my own custom model binder?
If I do:
public ActionResult Save(ExpandoObject form)
{
....
}
The value of form is null.
Or if I have:
public class MyDynamicThing : DynamicObject
{
public int Id { get; set; }
public override bool TrySetMember(SetMemberBinder binder, object value)
{
// Set breakpoint here but doesn't get hit when model binding
return base.TrySetMember(binder, value);
}
}
...and in my controller:
public ActionResult Save(MyDynamicThing form)
{
....
}
In the above example Id is set to the value from the form. However if I set a breakpoint in TrySetMember this doesn't get hit.
Are there any magical incantations I can invoke to coerce the built-in model binder to work with ExpandoObjects or my own classes derived from DynamicObject?
I could resort to picking up the raw form post collection but I have to serialise this data to JSON which would mean an extra and untidy step to harvest those values.
No, this is not possible with the built-in model binder. You could of course write a custom model binder. The only property that the built-in model binder is capable of binding is the one that it seems from the MyDynamicThing type and that's why it can only set the Id property. It has no knowledge of other properties.
Try this:
public class ExpandoObjectBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("bindingContext");
IDictionary<string, object> model = new ExpandoObject();
string modelName = bindingContext.ModelName;
var form = controllerContext.HttpContext.Request.Unvalidated.Form;
var keys = form.AllKeys.Where(k => k.StartsWith(modelName + "."));
Debug.Write("ExpandoObjectBinder keys count is " + keys.Count());
foreach (var key in keys)
{
var propName = key.Replace(model + ".", "");
model.Add(propName, form[key]);
}
if (model.Count == 0)
throw new Exception("Data is empty.");
return model;
}
}
Register the binder mvc initialization :
ModelBinders.Binders.Add(typeof(ExpandoObject), new ExpandoObjectBinder());
Type support added (int, byte, long, decimal, string, array and subobjects):
public class ExpandoObjectBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException("bindingContext");
string modelName = bindingContext.ModelName;
var form = controllerContext.HttpContext.Request.Unvalidated.Form;
var model = ParseProperties(modelName, form);
return model;
}
public object ParseProperties(string modelName, NameValueCollection form)
{
var keys = form.AllKeys.Where(k => k.StartsWith(modelName + "."));
Debug.WriteLine("ExpandoObjectBinder keys count is " + keys.Count() + " for " + modelName);
IDictionary<string, object> model = new ExpandoObject();
List<string> subModelNames = new List<string>();
List<string> arrModelNames = new List<string>();
//ModelName: Dialog.Attributes[0].Options
foreach (var key in keys)
{
//Dialog.Attributes[0].Options.Byte
//Dialog.Attributes[0].Options.Inner.Byte
//Dialog.Attributes[0].Options.Inner.Integer
//Dialog.Attributes[0].Options.SimpleArray[0]
//Dialog.Attributes[0].Options.SimpleArray[1]
var propName = key.Replace(modelName + ".", "");
//Byte
//Inner.Byte
//Inner.Integer
//SimpleArray[0]
//SimpleArray[1]
if (!(propName.Contains('[') || propName.Contains('.')))
{
model.Add(propName, GetValue(form, key));
}
//array (can allow sub objects)
if (propName.Contains('['))
{
var names = propName.Split('[');
var arrModelName = names[0];
if (!arrModelNames.Contains(arrModelName))
arrModelNames.Add(arrModelName);
}
//not array but can has sub objects
if (!propName.Contains('[') && propName.Contains('.'))
{
var names = propName.Split('.');
var subModelName = names[0];
if (!subModelNames.Contains(subModelName))
subModelNames.Add(subModelName);
}
}
foreach (var subModelName in subModelNames)
{
var key = modelName + "." + subModelName;
object val = form[key];
val = ParseProperties(key, form);
model.Add(subModelName, val);
}
foreach (var arrModelName in arrModelNames)
{
//Dialog.Attributes[0].Options.SimpleArray[
var key = modelName + "." + arrModelName + "[";
var arrKeys = form.AllKeys.Where(k => k.StartsWith(key));
var isComplexArray = false;
int length = 0;
foreach (var arrKey in arrKeys)
{
var aKey = arrKey.Replace(key, "");
if (aKey.Contains("."))
isComplexArray = true;
var parsed = aKey.Split(']');
var num = int.Parse(parsed[0]);
if (num > length)
length = num;
}
List<object> vals = new List<object>();
if (isComplexArray)
{
for (int i = 0; i < length + 1; i++)
{
var arrKey = key + i + "]";
object val = ParseProperties(arrKey, form);
vals.Add(val);
}
}
else
{
for (int i = 0; i < length + 1; i++)
{
var arrKey = key + i + "]";
vals.Add(GetValue(form, arrKey));
}
}
model.Add(arrModelName, vals);
}
return model;
}
object GetValue(NameValueCollection form, string key)
{
object val = form[key];
if (decimal.TryParse(form[key], out decimal decimalVal))
val = decimalVal;
if (long.TryParse(form[key], out long longVal))
val = longVal;
if (int.TryParse(form[key], out int intVal))
val = intVal;
if (byte.TryParse(form[key], out byte byteVal))
val = byteVal;
if (bool.TryParse(form[key], out bool boolVal))
val = boolVal;
return val;
}
}
Related
I'm working with some application designed using ASP.NET MVC.
Did spend lot of time trying to solve some problem, but do not have idea what to do to solve it.
As similar code shown below for big JSON will throw exception :
"Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property.
"
EXAMPLE :
$http.post('/API/PostData',aoData)...
where aoData equals 3K array of JSON, etc.
Added some solution suggested in many questions being asked on stackoverflow.
Did solve that problem just by :
Removing JsonValueProviderFactory from the ValueProviderFactories.Factories
And adding copy of the original class with simple modification such as :
EXAMPLE:
public sealed class LargeJsonValueProviderFactory : ValueProviderFactory
{
private static void AddToBackingStore(LargeJsonValueProviderFactory.EntryLimitedDictionary backingStore, string prefix, object value)
{
IDictionary<string, object> dictionary = value as IDictionary<string, object>;
if (dictionary != null)
{
foreach (KeyValuePair<string, object> keyValuePair in (IEnumerable<KeyValuePair<string, object>>) dictionary)
LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakePropertyKey(prefix, keyValuePair.Key), keyValuePair.Value);
}
else
{
IList list = value as IList;
if (list != null)
{
for (int index = 0; index < list.Count; ++index)
LargeJsonValueProviderFactory.AddToBackingStore(backingStore, LargeJsonValueProviderFactory.MakeArrayKey(prefix, index), list[index]);
}
else
backingStore.Add(prefix, value);
}
}
private static object GetDeserializedObject(ControllerContext controllerContext)
{
if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
return (object) null;
string end = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();
if (string.IsNullOrEmpty(end))
return (object) null;
var serializer = new JavaScriptSerializer {MaxJsonLength = Int32.MaxValue};
return serializer.DeserializeObject(end);
}
/// <summary>Returns a JSON value-provider object for the specified controller context.</summary>
/// <returns>A JSON value-provider object for the specified controller context.</returns>
/// <param name="controllerContext">The controller context.</param>
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
throw new ArgumentNullException("controllerContext");
object deserializedObject = LargeJsonValueProviderFactory.GetDeserializedObject(controllerContext);
if (deserializedObject == null)
return (IValueProvider) null;
Dictionary<string, object> dictionary = new Dictionary<string, object>((IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);
LargeJsonValueProviderFactory.AddToBackingStore(new LargeJsonValueProviderFactory.EntryLimitedDictionary((IDictionary<string, object>) dictionary), string.Empty, deserializedObject);
return (IValueProvider) new DictionaryValueProvider<object>((IDictionary<string, object>) dictionary, CultureInfo.CurrentCulture);
}
private static string MakeArrayKey(string prefix, int index)
{
return prefix + "[" + index.ToString((IFormatProvider) CultureInfo.InvariantCulture) + "]";
}
private static string MakePropertyKey(string prefix, string propertyName)
{
if (!string.IsNullOrEmpty(prefix))
return prefix + "." + propertyName;
return propertyName;
}
private class EntryLimitedDictionary
{
private static int _maximumDepth = LargeJsonValueProviderFactory.EntryLimitedDictionary.GetMaximumDepth();
private readonly IDictionary<string, object> _innerDictionary;
private int _itemCount;
public EntryLimitedDictionary(IDictionary<string, object> innerDictionary)
{
this._innerDictionary = innerDictionary;
}
public void Add(string key, object value)
{
if (++this._itemCount > LargeJsonValueProviderFactory.EntryLimitedDictionary._maximumDepth)
throw new InvalidOperationException("JsonValueProviderFactory_RequestTooLarge");
this._innerDictionary.Add(key, value);
}
private static int GetMaximumDepth()
{
NameValueCollection appSettings = ConfigurationManager.AppSettings;
if (appSettings != null)
{
string[] values = appSettings.GetValues("aspnet:MaxJsonDeserializerMembers");
int result;
if (values != null && values.Length > 0 && int.TryParse(values[0], out result))
return result;
}
return 1000;
}
}
}
And that solve the problem with maxJsonLength. Great! But...
If JSON contains property called ACTION, controller get data being changed. The ACTION property contains name of the controller's action instead of "MAR". The LargeJsonValueProviderFactory class does not change value of the ACION property. But if LargeJsonValueProviderFactory class shown above is not is use issue disappears.
EXAMPLE :
{
NR : 1200,
ACTION : "MAR",
.....
}
public ActionResult Save(PrsentationEntity aoData)
{
aoData.NR equals 1200 - OK
aoData.ACTION equals "Save" -Should be "MAR"
Do you have any ideas why I have that problem ?
Regards
Marcin
tl;dr
When configuring the application the original JsonValueProviderFactory should be replaced by the custom LargeJsonValueProviderFactory instead of added to the end of the collection.
Long version
You said you solved the problem by:
Removing JsonValueProviderFactory from the ValueProviderFactories.Factories
And adding copy of the original class with simple modification such as :
That's why the problem occurs.
The order of the factories on ValueProviderFactories.Factories does matter, but it's not generally discussed.
The original order is this:
private static readonly ValueProviderFactoryCollection _factories = new ValueProviderFactoryCollection()
{
new ChildActionValueProviderFactory(),
new FormValueProviderFactory(),
new JsonValueProviderFactory(),
new RouteDataValueProviderFactory(),
new QueryStringValueProviderFactory(),
new HttpFileCollectionValueProviderFactory(),
new JQueryFormValueProviderFactory()
};
If you just add your new provider to the end of the collection it won't be used if one of the other providers does the job (in this case it seems that RouteDataValueProviderFactory was used).
I got this simple model class:
public class PrvProduct
{
[Key]
public Int32 ProductId
{
get; set;
}
public Int64 ProductLineId;
public String MfgPartNumber;
public String ProductName;
public String ProductDescription;
}
I'm trying to call a stored proc, using .net core, it works fine, returns a list of PrvProduct objects. problem is: their fields are empty, unless I fill them up myself in code. ProductId is always there, not sure why (maybe because i typed there the [key] attribute?) but the rest are not.
is there a simple way to map class fields to results sets, like in ado.net (i would just do SQLDataAdapter.Fill(MyDataTable) and the MyDataTable fields will have the values by field name)... or do i have to do option 2 below every time?
Many thanks!
string sqlQuery = "EXEC Maint.GetProductList '" + sNameFilter + "'";
//option 1: this gets no value in the fields of each PrvProduct (ProductId gets value maybe because its [key], the others don't)
IQueryable results = _context.Products.FromSql(sqlQuery).AsNoTracking();
//option 2: this works, but... do i have to do this for every stored proc i call, every field, or is there a beter way to map class fields to returned results fields?
List<PrvProduct> oList = new List<PrvProduct>();
using (var command = _context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = sqlQuery;
command.CommandType = CommandType.Text;
_context.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
while (result.Read())
{
// Map to your entity
oList.Add(new PrvProduct
{
ProductId = result.GetInt32(0),
ProductName = result.GetString(1)
});
}
}
}
In EF Core, if you execute a stored procedure using one of your DbSet entities then it will map it automatically. The problem is that in many case you need to map a stored procedure to a DTO, for example, and the DTO is not part of your DbSet entities. In those cases you need to go back in time and map it manually which is a waste of time.
In order to avoid mapping the data reader manually, I added a bunch of extension methods that do it for you. The code is not perfect and I'm still improving it but it's good enough in most of the cases.
Once you add the extensions methods I'm gonna describe below, you can use it like this:
return dbContext.Database.SqlQuery<SalesReportDTO>("spGetSalesReport",
SqlParameterBuilder.Build("customerId", customerId),
SqlParameterBuilder.Build("dateFrom", from),
SqlParameterBuilder.Build("dateTo", to)).ToList();
DatabaseFacadeExtensions: adds extensions methods to DatabaseFacade class, allowing you to call the method SqlQuery from dbContext.Database just like we used to do with Entity Framework 6.
public static class DatabaseFacadeExtensions
{
public static List<T> SqlQuery<T>(this DatabaseFacade database, string query, params SqlParameter[] parameters)
{
return SqlQuery<T>(database, query, null, CommandType.StoredProcedure, parameters);
}
public static List<T> SqlQuery<T>(this DatabaseFacade database, string query, CommandType commandType, params SqlParameter[] parameters)
{
return SqlQuery<T>(database, query, null, commandType, parameters);
}
public static List<T> SqlQuery<T>(this DatabaseFacade database, string query, int? commandTimeout, params SqlParameter[] parameters)
{
return SqlQuery<T>(database, query, commandTimeout, CommandType.StoredProcedure, parameters);
}
public static List<T> SqlQuery<T>(this DatabaseFacade database, string query, int? commandTimeout, CommandType commandType, params SqlParameter[] parameters)
{
using (var cmd = database.GetDbConnection().CreateCommand())
{
cmd.CommandText = query;
cmd.CommandType = commandType;
if (commandTimeout.HasValue)
{
cmd.CommandTimeout = commandTimeout.Value;
}
cmd.Parameters.AddRange(parameters);
if (cmd.Connection.State == System.Data.ConnectionState.Closed)
{
cmd.Connection.Open();
}
try
{
using (var reader = cmd.ExecuteReader())
{
return reader.MapToList<T>();
}
}
finally
{
cmd.Connection.Close();
}
}
}
}
DbDataReaderExtensions: adds extensions methods to DbDataReader class so it can map the data reader to your own clases.
public static class DbDataReaderExtensions
{
public static List<T> MapToList<T>(this DbDataReader dr)
{
var objList = new List<T>();
if (dr.HasRows)
{
bool isSingleValue = typeof(T).IsPrimitive || typeof(T) == typeof(string);
IEnumerable<PropertyInfo> props = null;
Dictionary<string, DbColumn> colMapping = null;
if (!isSingleValue)
{
props = typeof(T).GetRuntimeProperties();
colMapping = dr.GetColumnSchema()
.Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
.ToDictionary(key => key.ColumnName.ToLower());
}
while (dr.Read())
{
T obj;
if (isSingleValue)
{
obj = (T)dr.GetValue(0);
}
else
{
obj = Activator.CreateInstance<T>();
foreach (var prop in props)
{
string propertyName = prop.Name.ToLower();
if (!colMapping.ContainsKey(propertyName))
{
continue;
}
var val = dr.GetValue(colMapping[propertyName].ColumnOrdinal.Value);
if (val != DBNull.Value)
{
// enum property
if (prop.PropertyType.IsEnum)
{
prop.SetValue(obj, Enum.ToObject(prop.PropertyType, val));
}
// nullable enum property
if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>) && Nullable.GetUnderlyingType(prop.PropertyType).IsEnum)
{
prop.SetValue(obj, Enum.ToObject(Nullable.GetUnderlyingType(prop.PropertyType), val));
}
else
{
prop.SetValue(obj, val);
}
}
}
}
objList.Add(obj);
}
}
return objList;
}
public static T MapToObject<T>(this DbDataReader dr)
{
var props = typeof(T).GetRuntimeProperties();
if (dr.HasRows)
{
var colMapping = dr.GetColumnSchema()
.Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
.ToDictionary(key => key.ColumnName.ToLower());
if (dr.Read())
{
T obj = Activator.CreateInstance<T>();
foreach (var prop in props)
{
var val = dr.GetValue(colMapping[prop.Name.ToLower()].ColumnOrdinal.Value);
prop.SetValue(obj, val == DBNull.Value ? null : val);
}
return obj;
}
}
return default(T);
}
}
The next class is optional but I use to build parameters in a simpler way and it's needed in the example I described above:
public class SqlParameterBuilder
{
public static SqlParameter Build(string name, bool? value)
{
if (value.HasValue)
{
return new SqlParameter() { ParameterName = name, Value = value.Value };
}
return new SqlParameter() { ParameterName = name, Value = DBNull.Value };
}
public static SqlParameter Build(string name, int? value)
{
if (value.HasValue)
{
return new SqlParameter() { ParameterName = name, Value = value.Value };
}
return new SqlParameter() { ParameterName = name, Value = DBNull.Value };
}
public static SqlParameter Build(string name, string value)
{
if (value != null)
{
return new SqlParameter() { ParameterName = name, Value = value };
}
return new SqlParameter() { ParameterName = name, Value = DBNull.Value };
}
public static SqlParameter Build(string name, DateTime? value)
{
if (value != null)
{
return new SqlParameter { ParameterName = name, SqlDbType = SqlDbType.DateTime, Value = value };
}
return new SqlParameter() { ParameterName = name, Value = DBNull.Value };
}
public static SqlParameter Build(string name, Guid? value)
{
if (value.HasValue)
{
return new SqlParameter { ParameterName = name, SqlDbType = SqlDbType.UniqueIdentifier, Value = value };
}
return new SqlParameter() { ParameterName = name, Value = DBNull.Value };
}
public static SqlParameter Build(string name, int[] values)
{
SqlParameter par = new SqlParameter(name, SqlDbType.Structured);
par.TypeName = "dbo.IntParameterList";
DataTable dt = new DataTable();
dt.Columns.Add("id", typeof(int));
par.Value = dt;
if (values != null)
{
foreach (int value in values.Where(p => p != 0))
{
dt.Rows.Add(value);
}
}
return par;
}
public static SqlParameter Build(string name, string[] values, VarcharParameterListEnum varcharParameterListType = VarcharParameterListEnum.Varchar50)
{
SqlParameter par = new SqlParameter(name, SqlDbType.Structured);
switch(varcharParameterListType)
{
case VarcharParameterListEnum.Varchar15:
par.TypeName = "dbo.Varchar15ParameterList";
break;
case VarcharParameterListEnum.Varchar50:
par.TypeName = "dbo.Varchar50ParameterList";
break;
case VarcharParameterListEnum.Varchar100:
par.TypeName = "dbo.Varchar100ParameterList";
break;
case VarcharParameterListEnum.Varchar255:
par.TypeName = "dbo.Varchar255ParameterList";
break;
case VarcharParameterListEnum.Varchar510:
par.TypeName = "dbo.Varchar510ParameterList";
break;
}
DataTable dt = new DataTable();
dt.Columns.Add("textValue", typeof(string));
par.Value = dt;
if (values != null)
{
foreach (var value in values.Where(p => !string.IsNullOrWhiteSpace(p)))
{
dt.Rows.Add(value);
}
}
return par;
}
}
I have the following helper method in a ViewModelBase class, which is inherited by other view Models:
public string GetEnumName<T>(Enum value)
{
Type enumType = typeof(T);
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
var outString = ((DisplayAttribute)attrs[0]).Name;
if (((DisplayAttribute)attrs[0]).ResourceType != null)
{
outString = ((DisplayAttribute)attrs[0]).GetName();
}
return outString;
}
I then call this from the view like this:
<p>
#{var rel = Model.GetEnumDisplayName<Enums.wheteverEnum>(Model.wheteverEnum); }
#rel
</p>
Question is - can I work this method so I don't have to tell it the type of the enum? Basically I'd like todo this for all enums:
#Model.GetEnumDisplayName(Model.wheteverEnum)
No typeof, no T, no need to add a reference to the Enums namespace in the View...
Possible?
You can simply remove the type parameter and make it an extension method.
public static string DisplayName(this Enum value)
{
Type enumType = value.GetType();
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
var outString = ((DisplayAttribute)attrs[0]).Name;
if (((DisplayAttribute)attrs[0]).ResourceType != null)
{
outString = ((DisplayAttribute)attrs[0]).GetName();
}
return outString;
}
#Model.wheteverEnum.DisplayName()
Could you not write this as an extension method? Something like...
public static class EnumExtensions
{
public static string ToDescription(this Enum e)
{
var attributes = (DisplayAttribute[])e.GetType().GetField(e.ToString()).GetCustomAttributes(typeof(DisplayAttribute), false);
return attributes.Length > 0 ? attributes[0].Description : string.Empty;
}
}
Usage:
#Model.WhateverEnum.ToDescription();
Nice work #jrummell!
I've added a small tweak below which captures the scenario where an enum doesn't have an associated Display attribute (currently it throws an exception)
/// <summary>
/// Gets the DataAnnotation DisplayName attribute for a given enum (for displaying enums values nicely to users)
/// </summary>
/// <param name="value">Enum value to get display for</param>
/// <returns>Pretty version of enum (if there is one)</returns>
/// <remarks>
/// Inspired by :
/// http://stackoverflow.com/questions/9328972/mvc-net-get-enum-display-name-in-view-without-having-to-refer-to-enum-type-in-vi
/// </remarks>
public static string DisplayFor(this Enum value) {
Type enumType = value.GetType();
var enumValue = Enum.GetName(enumType, value);
MemberInfo member = enumType.GetMember(enumValue)[0];
string outString = "";
var attrs = member.GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs.Any()) {
var displayAttr = ((DisplayAttribute)attrs[0]);
outString = displayAttr.Name;
if (displayAttr.ResourceType != null) {
outString = displayAttr.GetName();
}
} else {
outString = value.ToString();
}
return outString;
}
The answer of #jrummell in VB.NET for the few of us...
Module ModuleExtension
<Extension()>
Public Function DisplayName(ByVal value As System.Enum) As String
Dim enumType As Type = value.GetType()
Dim enumValue = System.Enum.GetName(enumType, value)
Dim member As MemberInfo = enumType.GetMember(enumValue)(0)
Dim attrs = member.GetCustomAttributes(GetType(DisplayAttribute), False)
Dim outString = CType(attrs(0), DisplayAttribute).Name
If (CType(attrs(0), DisplayAttribute).ResourceType IsNot Nothing) Then
outString = CType(attrs(0), DisplayAttribute).GetName()
End If
Return outString
End Function
End Module
for anyone who might reach to this question, I found this a lot easier than any thing else:
https://www.codeproject.com/articles/776908/dealing-with-enum-in-mvc
Just create a folder "DisplayTemplate" under "Views\Shared", and create an empty view (Name it "Enum") in the new folder "DisplayTemplate", and copy this code to it"
#model Enum
#if (EnumHelper.IsValidForEnumHelper(ViewData.ModelMetadata))
{
// Display Enum using same names (from [Display] attributes) as in editors
string displayName = null;
foreach (SelectListItem item in EnumHelper.GetSelectList(ViewData.ModelMetadata, (Enum)Model))
{
if (item.Selected)
{
displayName = item.Text ?? item.Value;
}
}
// Handle the unexpected case that nothing is selected
if (String.IsNullOrEmpty(displayName))
{
if (Model == null)
{
displayName = String.Empty;
}
else
{
displayName = Model.ToString();
}
}
#Html.DisplayTextFor(model => displayName)
}
else
{
// This Enum type is not supported. Fall back to the text.
#Html.DisplayTextFor(model => model)
}
Here is an extension method that I've written to do just this... it has a little extra logic in it to parse Enum names and split by capital letters. You can override any name by using the Display Attribute
public static TAttribute GetAttribute<TAttribute>(this ICustomAttributeProvider parameterInfo) where TAttribute : Attribute
{
object[] attributes = parameterInfo.GetCustomAttributes(typeof(TAttribute), false);
return attributes.Length > 0 ? (TAttribute)attributes[0] : null;
}
public static bool HasAttribute<TAttribute>(this ICustomAttributeProvider parameterInfo) where TAttribute : Attribute
{
object[] attributes = parameterInfo.GetCustomAttributes(typeof(TAttribute), false);
return attributes.Length > 0 ? true : false;
}
public static string ToFriendlyEnum(this Enum type)
{
return type.GetType().HasAttribute<DescriptionAttribute>() ? type.GetType().GetAttribute<DescriptionAttribute>().Description : type.ToString().ToFriendlyEnum();
}
public static string ToFriendlyEnum(this string value)
{
char[] chars = value.ToCharArray();
string output = string.Empty;
for (int i = 0; i < chars.Length; i++)
{
if (i <= 0 || chars[i - 1].ToString() != chars[i - 1].ToString().ToUpper() && chars[i].ToString() != chars[i].ToString().ToLower())
{
output += " ";
}
output += chars[i];
}
return output.Trim();
}
The GetAttribute extension methods could be slightly overkill, but I use them elsewhere in my projects, so they got reused when I wrote my Enum extension. You could easily combine them back into the ToFriendlyEnum(this Enum type) method
The suggested sollutions does not worked for me with MVC3: so the helper below is good.:
public static string GetEnumDescription(this Enum value)
{
Type type = value.GetType();
string name = Enum.GetName(type, value);
if (name != null)
{
FieldInfo field = type.GetField(name);
if (field != null)
{
string attr = field.GetCustomAttributesData()[0].NamedArguments[0].TypedValue.Value.ToString();
if (attr == null)
{
return name;
}
else
{
return attr;
}
}
}
return null;
}
I wrote a helper method to display enums from my model in my asp.net MVC application as drop down lists in my views.
Here is my code for that:
public static List<SelectListItem> CreateSelectItemList<TEnum>(object enumObj,
string defaultItemKey,
bool sortAlphabetically,
object firstValueOverride)
where TEnum : struct
{
var values = (from v in (TEnum[])Enum.GetValues(typeof(TEnum))
select new
{
Id = Convert.ToInt32(v),
Name = ResourceHelpers.GetResourceValue(AppConstants.EnumResourceNamespace,
typeof(TEnum).Name, v.ToString())
});
return values.ToSelectList(e => e.Name,
e => e.Id.ToString(),
!string.IsNullOrEmpty(defaultItemKey) ? ResourceHelpers.GetResourceValue(AppConstants.EnumResourceNamespace, defaultItemKey) : string.Empty,
enumObj,
sortAlphabetically,
firstValueOverride);
}
This actually generates the select item list:
public static List<SelectListItem> ToSelectList<T>(
this IEnumerable<T> enumerable,
Func<T, string> text,
Func<T, string> value,
string defaultOption,
object selectedVal,
bool sortAlphabetically,
object FirstValueOverride)
{
int iSelectedVal = -1;
if(selectedVal!=null)
{
try
{
iSelectedVal = Convert.ToInt32(selectedVal);
}
catch
{
}
}
var items = enumerable.Select(f => new SelectListItem()
{
Text = text(f),
Value = value(f),
Selected = (iSelectedVal > -1)? (iSelectedVal.ToString().Equals(value(f))) : false
});
#region Sortare alfabetica
if (sortAlphabetically)
items = items.OrderBy(t => t.Text);
#endregion Sortare alfabetica
var itemsList = items.ToList();
Func<SelectListItem, bool> funct = null;
string sValue = string.Empty;
SelectListItem firstItem = null;
SelectListItem overridenItem = null;
int overridenIndex = 0;
if (FirstValueOverride != null)
{
sValue = FirstValueOverride.ToString();
funct = (t => t.Value == sValue);
overridenItem = itemsList.SingleOrDefault(funct);
overridenIndex = itemsList.IndexOf(overridenItem);
if (overridenItem != null)
{
firstItem = itemsList.ElementAt(0);
itemsList[0] = overridenItem;
itemsList[overridenIndex] = firstItem;
}
}
if(!string.IsNullOrEmpty(defaultOption))
itemsList.Insert(0, new SelectListItem()
{
Text = defaultOption,
Value = "-1"
});
return itemsList;
}
These is the method I call:
public static MvcHtmlString EnumDropDownList<TEnum>(this HtmlHelper htmlHelper,
object enumObj,
string name,
string defaultItemKey,
bool sortAlphabetically,
object firstValueOverride,
object htmlAttributes)
where TEnum : struct
{
return htmlHelper.DropDownList(name,
CreateSelectItemList<TEnum>(enumObj,
defaultItemKey,
sortAlphabetically,
firstValueOverride),
htmlAttributes);
}
Now I am having the problem described here
When I call this helper method and the input's name is the same as the property's name the selected value doesn't get selected.
The alternate solution described there doesn't work for me. The only solution that works is changing the name and not using the model binding using FormCollection instead.
I don't like this workaround because I can't use validation any more using the ViewModel pattern and I have to write some extra code for every enum.
I tried writing a custom model binder to compensate for this somehow but none of the methods I can override there gets called when I start the action.
Is there any way to do this without using FormCollection?
Can I somehow intercept ASP.NET MVC when it tries to put the value into my input field and make it select the right value?
Thank you in advance.
I have a sollution now
Here is the code for the EnumDropDownList function the other functions are listed in the question:
public static MvcHtmlString EnumDropDownList(this HtmlHelper htmlHelper,
object enumObj,
string name,
string defaultItemKey,
bool sortAlphabetically,
object firstValueOverride,
object htmlAttributes)
{
StringBuilder sbRezultat = new StringBuilder();
int selectedIndex = 0;
var selectItemList = new List<SelectListItem>();
if (enumObj != null)
{
selectItemList = CreateSelectItemList(enumObj, defaultItemKey, true, null);
var selectedItem = selectItemList.SingleOrDefault(item => item.Selected);
if (selectedItem != null)
{
selectedIndex = selectItemList.IndexOf(selectedItem);
}
}
var dict = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
TagBuilder tagBuilder = new TagBuilder("select");
tagBuilder.MergeAttribute("name", name,true);
bool bReadOnly = false;
//special case for readonly
if(dict.ContainsKey("readonly"))
{
//remove this tag it won't work the way mvc renders it anyway
dict.Remove("readonly");
bReadOnly = true;
}
//in case the style element is completed if the drop down is not readonly
tagBuilder.MergeAttributes(dict, true);
if (bReadOnly)
{
//add a small javascript to make it readonly and add the lightgrey style
tagBuilder.MergeAttribute("onchange", "this.selectedIndex=" + selectedIndex + ";",true);
tagBuilder.MergeAttribute("style", "background: lightgrey", true);
}
sbRezultat.Append(tagBuilder.ToString(TagRenderMode.StartTag));
foreach (var option in selectItemList)
{
sbRezultat.Append(" <option value='");
sbRezultat.Append(option.Value);
sbRezultat.Append("' ");
if (option.Selected)
sbRezultat.Append("selected");
sbRezultat.Append(" >");
sbRezultat.Append(option.Text);
sbRezultat.Append("</option>");
}
sbRezultat.Append("</select>");
return new MvcHtmlString(sbRezultat.ToString());
}
I also wrote a generic function of type For (EnumDropDownFor):
public static MvcHtmlString EnumDropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
string defaultItemKey,
bool sortAlphabetically,
object firstValueOverride,
object htmlAttributes)
where TProperty : struct
{
string inputName = GetInputName(expression);
object selectedVal = null;
try
{
selectedVal = htmlHelper.ViewData.Model == null
? default(TProperty)
: expression.Compile()(htmlHelper.ViewData.Model);
}
catch//in caz ca e ceva null sau ceva de genu'
{
}
return EnumDropDownList(htmlHelper,
selectedVal,
inputName,
defaultItemKey,
sortAlphabetically,
firstValueOverride,
htmlAttributes);
}
and some helper methods:
public static string GetInputName<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
if (expression.Body.NodeType == ExpressionType.Call)
{
MethodCallExpression methodCallExpression = (MethodCallExpression)expression.Body;
string name = GetInputName(methodCallExpression);
return name.Substring(expression.Parameters[0].Name.Length + 1);
}
return expression.Body.ToString().Substring(expression.Parameters[0].Name.Length + 1);
}
private static string GetInputName(MethodCallExpression expression)
{
MethodCallExpression methodCallExpression = expression.Object as MethodCallExpression;
if (methodCallExpression != null)
{
return GetInputName(methodCallExpression);
}
return expression.Object.ToString();
}
How can I enumerate through all the key/values of
a FormCollection (system.web.mvc) in ASP.NET MVC?
Here are 3 ways to do it specifically with a FormCollection object.
public ActionResult SomeActionMethod(FormCollection formCollection)
{
foreach (var key in formCollection.AllKeys)
{
var value = formCollection[key];
}
foreach (var key in formCollection.Keys)
{
var value = formCollection[key.ToString()];
}
// Using the ValueProvider
var valueProvider = formCollection.ToValueProvider();
foreach (var key in valueProvider.Keys)
{
var value = valueProvider[key];
}
}
foreach(KeyValuePair<string, ValueProviderResult> kvp in form.ToValueProvider())
{
string htmlControlName = kvp.Key;
string htmlControlValue = kvp.Value.AttemptedValue;
}
foreach(var key in Request.Form.AllKeys)
{
var value = Request.Form[key];
}
In .NET Framework 4.0, the code to use the ValueProvider is:
IValueProvider valueProvider = formValues.ToValueProvider();
foreach (string key in formValues.Keys)
{
ValueProviderResult result = valueProvider.GetValue(key);
string value = result.AttemptedValue;
}
I use this:
string keyname;
string keyvalue;
for (int i = 0; i <= fc.Count - 1; i++)
{
keyname = fc.AllKeys[i];
keyvalue = fc[i];
}
hope it helps someone.
And in VB.Net:
Dim fv As KeyValuePair(Of String, ValueProviderResult)
For Each fv In formValues.ToValueProvider
Response.Write(fv.Key + ": " + fv.Value.AttemptedValue)
Next