This question already has answers here:
String representation of an Enum
(37 answers)
Localizing enum descriptions attributes
(8 answers)
Closed 9 years ago.
I have an enumeration like
Enum Complexity
{
NotSoComplex,
LittleComplex,
Complex,
VeryComplex
}
And I want to use it in a dropdown list, but don't want to see such Camel names in list (looks really odd for users). Instead I would like to have in normal wording, like
Not so complex
Little complex (etc)
Also, my application is multi-lang and I would like to be able to display those strings localized, and I use a helper, TranslationHelper(string strID) which gives me the localized version for a string id.
I have a working solution, but not very elegant:
I create a helper class for the enum, with one member Complexity and ToString() overwritten, like below (code simplified)
public class ComplexityHelper
{
public ComplexityHelper(Complexity c, string desc)
{ m_complex = c; m_desc=desc; }
public Complexity Complexity { get { ... } set {...} }
public override ToString() { return m_desc; }
//Then a static field like this
private static List<Complexity> m_cxList = null;
// and method that returns the status lists to bind to DataSource of lists
public static List<ComplexityHelper> GetComplexities()
{
if (m_cxList == null)
{
string[] list = TranslationHelper.GetTranslation("item_Complexities").Split(',');
Array listVal = Enum.GetValues(typeof(Complexities));
if (list.Length != listVal.Length)
throw new Exception("Invalid Complexities translations (item_Complexities)");
m_cxList = new List<Complexity>();
for (int i = 0; i < list.Length; i++)
{
Complexity cx = (ComplexitylistVal.GetValue(i);
ComplexityHelper ch = new ComplexityHelper(cx, list[i]);
m_cxList.Add(ch);
}
}
return m_cxList;
}
}
While workable, I'm not happy with it, since I have to code it similarily for various enums I need to use in picklists.
Does anyone have a suggestion for a simpler or more generic solution?
Thanks
Bogdan
Basic Friendly names
Use the Description attribute:*
enum MyEnum
{
[Description("This is black")]
Black,
[Description("This is white")]
White
}
And a handy extension method for enums:
public static string GetDescription(this Enum value)
{
FieldInfo field = value.GetType().GetField(value.ToString());
object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true);
if(attribs.Length > 0)
{
return ((DescriptionAttribute)attribs[0]).Description;
}
return string.Empty;
}
Used like so:
MyEnum val = MyEnum.Black;
Console.WriteLine(val.GetDescription()); //writes "This is black"
(Note this doesn't exactly work for bit flags...)
For localization
There is a well-established pattern in .NET for handling multiple languages per string value - use a resource file, and expand the extension method to read from the resource file:
public static string GetDescription(this Enum value)
{
FieldInfo field = value.GetType().GetField(value.ToString());
object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true));
if(attribs.Length > 0)
{
string message = ((DescriptionAttribute)attribs[0]).Description;
return resourceMgr.GetString(message, CultureInfo.CurrentCulture);
}
return string.Empty;
}
Any time we can leverage existing BCL functionality to achieve what we want, that's definitely the first route to explore. This minimizes complexity and uses patterns already familiar to many other developers.
Putting it all together
To get this to bind to a DropDownList, we probably want to track the real enum values in our control and limit the translated, friendly name to visual sugar. We can do so by using an anonymous type and the DataField properties on the list:
<asp:DropDownList ID="myDDL"
DataTextField="Description"
DataValueField="Value" />
myDDL.DataSource = Enum.GetValues(typeof(MyEnum)).OfType<MyEnum>().Select(
val => new { Description = val.GetDescription(), Value = val.ToString() });
myDDL.DataBind();
Let's break down that DataSource line:
First we call Enum.GetValues(typeof(MyEnum)), which gets us a loosely-typed Array of the values
Next we call OfType<MyEnum>() which converts the array to an IEnumerable<MyEnum>
Then we call Select() and provide a lambda that projects a new object with two fields, Description and Value.
The DataTextField and DataValueField properties are evaluated reflectively at databind-time, so they will search for fields on DataItem with matching names.
-Note in the main article, the author wrote their own DescriptionAttribute class which is unnecessary, as one already exists in .NET's standard libraries.-
The use of attributes as in the other answers is a good way to go, but if you just want to use the text from the values of the enum, the following code will split based on the camel-casing of the value:
public static string GetDescriptionOf(Enum enumType)
{
Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
return capitalLetterMatch.Replace(enumType.ToString(), " $&");
}
Calling GetDescriptionOf(Complexity.NotSoComplex) will return Not So Complex. This can be used with any enum value.
To make it more useful, you could make it an extension method:
public static string ToFriendlyString(this Enum enumType)
{
Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
return capitalLetterMatch.Replace(enumType.ToString(), " $&");
}
You cal now call it using Complexity.NotSoComplex.ToFriendlyString() to return Not So Complex.
EDIT: just noticed that in your question you mention that you need to localise the text. In that case, I'd use an attribute to contain a key to look up the localised value, but default to the friendly string method as a last resort if the localised text cannot be found. You would define you enums like this:
enum Complexity
{
[LocalisedEnum("Complexity.NotSoComplex")]
NotSoComplex,
[LocalisedEnum("Complexity.LittleComplex")]
LittleComplex,
[LocalisedEnum("Complexity.Complex")]
Complex,
[LocalisedEnum("Complexity.VeryComplex")]
VeryComplex
}
You would also need this code:
[AttributeUsage(AttributeTargets.Field, AllowMultiple=false, Inherited=true)]
public class LocalisedEnum : Attribute
{
public string LocalisationKey{get;set;}
public LocalisedEnum(string localisationKey)
{
LocalisationKey = localisationKey;
}
}
public static class LocalisedEnumExtensions
{
public static string ToLocalisedString(this Enum enumType)
{
// default value is the ToString();
string description = enumType.ToString();
try
{
bool done = false;
MemberInfo[] memberInfo = enumType.GetType().GetMember(enumType.ToString());
if (memberInfo != null && memberInfo.Length > 0)
{
object[] attributes = memberInfo[0].GetCustomAttributes(typeof(LocalisedEnum), false);
if (attributes != null && attributes.Length > 0)
{
LocalisedEnum descriptionAttribute = attributes[0] as LocalisedEnum;
if (description != null && descriptionAttribute != null)
{
string desc = TranslationHelper.GetTranslation(descriptionAttribute.LocalisationKey);
if (desc != null)
{
description = desc;
done = true;
}
}
}
}
if (!done)
{
Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
description = capitalLetterMatch.Replace(enumType.ToString(), " $&");
}
}
catch
{
description = enumType.ToString();
}
return description;
}
}
To get the localised descriptions, you would then call:
Complexity.NotSoComplex.ToLocalisedString()
This has several fallback cases:
if the enum has a LocalisedEnum attribute defined, it will use the key to look up the translated text
if the enum has a LocalisedEnum attribute defined but no localised text is found, it defaults to using the camel-case split method
if the enum does not have a LocalisedEnum attribute defined, it will use the camel-case split method
upon any error, it defaults to the ToString of the enum value
I use the following class
public class EnumUtils
{
/// <summary>
/// Reads and returns the value of the Description Attribute of an enumeration value.
/// </summary>
/// <param name="value">The enumeration value whose Description attribute you wish to have returned.</param>
/// <returns>The string value portion of the Description attribute.</returns>
public static string StringValueOf(Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
DescriptionAttribute[] attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attributes.Length > 0)
{
return attributes[0].Description;
}
else
{
return value.ToString();
}
}
/// <summary>
/// Returns the Enumeration value that has a given Description attribute.
/// </summary>
/// <param name="value">The Description attribute value.</param>
/// <param name="enumType">The type of enumeration in which to search.</param>
/// <returns>The enumeration value that matches the Description value provided.</returns>
/// <exception cref="ArgumentException">Thrown when the specified Description value is not found with in the provided Enumeration Type.</exception>
public static object EnumValueOf(string value, Type enumType)
{
string[] names = Enum.GetNames(enumType);
foreach (string name in names)
{
if (StringValueOf((Enum)Enum.Parse(enumType, name)).Equals(value))
{
return Enum.Parse(enumType, name);
}
}
throw new ArgumentException("The string is not a description or value of the specified enum.");
}
Which reads an attribute called description
public enum PuppyType
{
[Description("Cute Puppy")]
CutePuppy = 0,
[Description("Silly Puppy")]
SillyPuppy
}
Thank you all for all answers.
Finally I used a combination from Rex M and adrianbanks, and added my own improvements, to simplify the binding to ComboBox.
The changes were needed because, while working on the code, I realized sometimes I need to be able to exclude one enumeration item from the combo.
E.g.
Enum Complexity
{
// this will be used in filters,
// but not in module where I have to assign Complexity to a field
AllComplexities,
NotSoComplex,
LittleComplex,
Complex,
VeryComplex
}
So sometimes I want the picklist to show all but AllComplexities (in add - edit modules) and other time to show all (in filters)
Here's what I did:
I created a extension method, that uses Description Attribute as localization lookup key. If Description attribute is missing, I create the lookup localization key as EnumName_
EnumValue. Finally, if translation is missing I just split enum name based on camelcase to separate words as shown by adrianbanks. BTW, TranslationHelper is a wrapper around resourceMgr.GetString(...)
The full code is shown below
public static string GetDescription(this System.Enum value)
{
string enumID = string.Empty;
string enumDesc = string.Empty;
try
{
// try to lookup Description attribute
FieldInfo field = value.GetType().GetField(value.ToString());
object[] attribs = field.GetCustomAttributes(typeof(DescriptionAttribute), true);
if (attribs.Length > 0)
{
enumID = ((DescriptionAttribute)attribs[0]).Description;
enumDesc = TranslationHelper.GetTranslation(enumID);
}
if (string.IsNullOrEmpty(enumID) || TranslationHelper.IsTranslationMissing(enumDesc))
{
// try to lookup translation from EnumName_EnumValue
string[] enumName = value.GetType().ToString().Split('.');
enumID = string.Format("{0}_{1}", enumName[enumName.Length - 1], value.ToString());
enumDesc = TranslationHelper.GetTranslation(enumID);
if (TranslationHelper.IsTranslationMissing(enumDesc))
enumDesc = string.Empty;
}
// try to format CamelCase to proper names
if (string.IsNullOrEmpty(enumDesc))
{
Regex capitalLetterMatch = new Regex("\\B[A-Z]", RegexOptions.Compiled);
enumDesc = capitalLetterMatch.Replace(value.ToString(), " $&");
}
}
catch (Exception)
{
// if any error, fallback to string value
enumDesc = value.ToString();
}
return enumDesc;
}
I created a generic helper class based on Enum, which allow to bind the enum easily to DataSource
public class LocalizableEnum
{
/// <summary>
/// Column names exposed by LocalizableEnum
/// </summary>
public class ColumnNames
{
public const string ID = "EnumValue";
public const string EntityValue = "EnumDescription";
}
}
public class LocalizableEnum<T>
{
private T m_ItemVal;
private string m_ItemDesc;
public LocalizableEnum(T id)
{
System.Enum idEnum = id as System.Enum;
if (idEnum == null)
throw new ArgumentException(string.Format("Type {0} is not enum", id.ToString()));
else
{
m_ItemVal = id;
m_ItemDesc = idEnum.GetDescription();
}
}
public override string ToString()
{
return m_ItemDesc;
}
public T EnumValue
{
get { return m_ID; }
}
public string EnumDescription
{
get { return ToString(); }
}
}
Then I created a generic static method that returns a List>, as below
public static List<LocalizableEnum<T>> GetEnumList<T>(object excludeMember)
{
List<LocalizableEnum<T>> list =null;
Array listVal = System.Enum.GetValues(typeof(T));
if (listVal.Length>0)
{
string excludedValStr = string.Empty;
if (excludeMember != null)
excludedValStr = ((T)excludeMember).ToString();
list = new List<LocalizableEnum<T>>();
for (int i = 0; i < listVal.Length; i++)
{
T currentVal = (T)listVal.GetValue(i);
if (excludedValStr != currentVal.ToString())
{
System.Enum enumVal = currentVal as System.Enum;
LocalizableEnum<T> enumMember = new LocalizableEnum<T>(currentVal);
list.Add(enumMember);
}
}
}
return list;
}
and a wrapper to return list with all members
public static List<LocalizableEnum<T>> GetEnumList<T>()
{
return GetEnumList<T>(null);
}
Now let's put all things together and bind to actual combo:
// in module where we want to show items with all complexities
// or just filter on one complexity
comboComplexity.DisplayMember = LocalizableEnum.ColumnNames.EnumValue;
comboComplexity.ValueMember = LocalizableEnum.ColumnNames.EnumDescription;
comboComplexity.DataSource = EnumHelper.GetEnumList<Complexity>();
comboComplexity.SelectedValue = Complexity.AllComplexities;
// ....
// and here in edit module where we don't want to see "All Complexities"
comboComplexity.DisplayMember = LocalizableEnum.ColumnNames.EnumValue;
comboComplexity.ValueMember = LocalizableEnum.ColumnNames.EnumDescription;
comboComplexity.DataSource = EnumHelper.GetEnumList<Complexity>(Complexity.AllComplexities);
comboComplexity.SelectedValue = Complexity.VeryComplex; // set default value
To read selected the value and use it, I use code as below
Complexity selComplexity = (Complexity)comboComplexity.SelectedValue;
Related
I'm working on a Window Forms application in Visual Studio, and I'm using a custom settings object to keep track of some application settings.
The user can change these settings through the PropertyGrid widget.
This works great for string and integer values, but now I also want to add a List<string> variable, so the user can enter a list of keywords.
I've added the List<string> variable to the settings object and I've added a TypeConverter to show it as a comma separated string representation in the PropertyGrid. Without the TypeConverter the value would display as just (Collection). It is displayed correctly and I can edit it, see screenshot below
this._MyProps = new PropsClass();
this._MyProps.ReadFromIniFile("mysettings.ini");
propertyGrid1.SelectedObject = this._MyProps;
Now I also want to write and read these setting to a settings.ini file, so I've added SaveToIniFile and ReadFromIniFile methods to the object. This works for string and integer values, except the List<string> is not saved and loaded to and from the .ini file correctly. When I call SaveToIniFile the content mysettings.ini is for example this, still using the "(Collection)" representation and not the values entered by the user:
[DataConvert]
KeyWordNull=NaN
ReplaceItemsList=(Collection)
YearMaximum=2030
So my question is, how can I save/load a List<string> setting to an ini file while also allowing the user to edit it in a PropertyGrid?
I know it'd have to convert from a string to a List somehow, maybe using quotes around the string to inclkude the line breaks, or maybe just comma-separated back to a list of values? But anyway I thought that is what the TypeConverter was for. So why is it showing correctly in he PropertyGrid but not in the ini file? See code below
The custom settings properties object:
// MyProps.cs
public class PropsClass
{
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[TypeConverter(typeof(StringListConverter))]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
and in the same PropsClass class, the write and read methods to save/load from a settings.ini file
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
and the TypeConverter class for the ReplaceItemsList property
public class StringListConverter : TypeConverter
{
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (value is List<string>)
{
return string.Join(",", ((List<string>)value).Select(x => x));
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
The reason your type converter is not used is because of this line:
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
You are getting the TypeConverter that is defined on the type of the property. So for ReplaceItemsList that would be the TypeConverter for List<T>. You need to get the TypeConverter for the property since that is where you added the TypeConverter attribute. So either you do something like you did for the category attribute in the read method where you use the PropertyInfo's GetCustomAttributes or you do what the PropertyGrid does which is use the PropertyDescriptors to get to the properties and their state. The latter would be better since if the object implemented ICustomTypeDescriptor or some other type augmentation like TypeDescriptionProvider then you would get that automatically.
So something like the following for the Save using PropertyDescriptors would be:
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in TypeDescriptor.GetProperties(this)
.Cast<PropertyDescriptor>()
.GroupBy(x => x.Attributes.Cast<Attribute>().OfType<CategoryAttribute>()
.FirstOrDefault()?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = propertyInfo.Converter;
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this)));
}
}
}
}
If you are going to use a custom TypeConverter, you'll have to register it as a provider to TypeDescriptionProvider:
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
And in your implementation you could just do this in the constructor of PropsClass (instead of using the attribute). I created some custom code below that would do the split.
public class PropsClass
{
[DllImport("kernel32.dll")]
public static extern int GetPrivateProfileSection(string lpAppName, byte[] lpszReturnBuffer, int nSize, string lpFileName);
[Description("Maximum year value."), Category("DataConvert"), DefaultValue(2050)]
public int YearMaximum { get; set; }
[Description("Null keyword, for example NaN or NULL, case sensitive."), Category("DataConvert"), DefaultValue("NULL")]
public string KeyWordNull { get; set; }
private List<string> _replaceItems = new List<string>();
[Description("List of items to replace."), Category("DataConvert"), DefaultValue("enter keywords here")]
[Editor("System.Windows.Forms.Design.StringCollectionEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public List<string> ReplaceItemsList
{
get
{
return _replaceItems;
}
set
{
_replaceItems = value;
}
}
public PropsClass()
{
TypeDescriptor.AddProvider(new CustumTypeDescriptorProvider(), typeof(List<string>));
}
public void SaveToIniFile(string filename)
{
// write to ini file
using (var fp = new StreamWriter(filename, false, Encoding.UTF8))
{
// for each different section
foreach (var section in GetType()
.GetProperties()
.GroupBy(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false)
.FirstOrDefault())?.Category ?? "General"))
{
fp.WriteLine(Environment.NewLine + "[{0}]", section.Key);
foreach (var propertyInfo in section.OrderBy(x => x.Name))
{
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
fp.WriteLine("{0}={1}", propertyInfo.Name, converter.ConvertToInvariantString(propertyInfo.GetValue(this, null)));
}
}
}
}
public void ReadFromIniFile(string filename)
{
// Load all sections from file
var loaded = GetType().GetProperties()
.Select(x => ((CategoryAttribute)x.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General")
.Distinct()
.ToDictionary(section => section, section => GetKeys(filename, section));
//var loaded = GetKeys(filename, "General");
foreach (var propertyInfo in GetType().GetProperties())
{
var category = ((CategoryAttribute)propertyInfo.GetCustomAttributes(typeof(CategoryAttribute), false).FirstOrDefault())?.Category ?? "General";
var name = propertyInfo.Name;
if (loaded.ContainsKey(category) && loaded[category].ContainsKey(name) && !string.IsNullOrEmpty(loaded[category][name]))
{
var rawString = loaded[category][name];
var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType);
if (converter.IsValid(rawString))
{
propertyInfo.SetValue(this, converter.ConvertFromString(rawString), null);
}
}
}
}
// helper function
private Dictionary<string, string> GetKeys(string iniFile, string category)
{
var buffer = new byte[8 * 1024];
GetPrivateProfileSection(category, buffer, buffer.Length, iniFile);
var tmp = Encoding.UTF8.GetString(buffer).Trim('\0').Split('\0');
return tmp.Select(x => x.Split(new[] { '=' }, 2))
.Where(x => x.Length == 2)
.ToDictionary(x => x[0], x => x[1]);
}
}
public class CustumTypeDescriptorProvider : TypeDescriptionProvider
{
public override ICustomTypeDescriptor GetTypeDescriptor(System.Type objectType, object instance)
{
if (objectType.Name == "List`1") return new StringListDescriptor();
return base.GetTypeDescriptor(objectType, instance);
}
}
public class StringListDescriptor : CustomTypeDescriptor
{
public override TypeConverter GetConverter()
{
return new StringListConverter();
}
}
public class StringListConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string s = value as string;
if (!string.IsNullOrEmpty(s))
{
return ((string)value).Split(',').ToList();
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string))
{
return string.Join(",", (List<string>)value);
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
*NOTE: In my testing, the method ConvertFrom is called twice, once from converter.IsValid and once from propertyInfo.SetValue.
*NOTE2: You are using streamwriter to update the ini file. Since you are using GetPrivateProfileSection, it seems you should be using WritePrivateProfileSection to update the ini file.
*NOTE3: please consider the original comments in question about whether you should be using this method to read/write to an ini file. These methods have been around a long time. (https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprivateprofilesection)
*NOTE4: A lot of this answer came from: Convert string to Array of string using TypeConverter
Leaving apart the PropertyGrid part which is just a way to put the info in a cartesian way on a Windows Form, the tricky part as I understand it, is to force a ini file (which is typically a 1:1 correlation between strings and strings) to house a collection of right hand side (rhs) values. Obviously, they cannot reside of a left hand side (lhs) of the ini line. You could, but then likely the choice of a ini file to persist your data is wrong at the roots.
So, why not avoiding to reinvent the wheel and go straight to custom parsing? This way you can also choose the separator to use (you may like to mimic windows, or apache setting files, etc).
Look at this implementation of one old code of mine. Grossly, it is an ini file telling my app which web exchange to contact for crypto trading. Before you get into a big headache with names: Monday is the name of the library holding the common code, and Friday is an app using Monday for trading online with a specific algo. Nothing important for the case being.
The ini file looks like this:
[Friday]
; if true the permutations will be only defined starting from the currency provided with index 0 in [Friday-Currencies]
MaxChainTransactions = 7
; if UseLastTradeValues is true, it will override UseConservativeBidAskValues
UseLastTradeValues = true
; if true the matrix values are (Ask+Bid)/2. This flag overrides UseConservativeBidAskValues but gets overridden by UseLastTradeValues
UseMidPriceValues = false
; if true the most conservative value between bid and ask will be used to calculate gains. Used only if UseLastTradeValues is false
UseConservativeBidAskValues = true
; if true, the solver will also solve with a matrix that will contain the best trade values from the available exchanges in parallel to other exchanges individually
MergeExchanges = false
; If true, Friday will issue one solution (if any) for each exchange, comprising one for the merged market if calculated.
DeliverOneSolutionPerExchange = true
; use values from the provided exchanges only
FridayExchanges = Bittrex| Binance | Kraken
; the minimum allowed gain remaining at the closure of the transaction chain to approve the full chain for ordering
MimimumAllowedGain = 1.05
; this is the target gain of a transaction. it is not used in case AllowPriceCorrectionBelowTargetGain is false
TargetGain = 1.09
; if true the prices in the transaction will be modified (increased towards the aggressive trade) of a factor equal to MaxPriceCorrectionFactor
AllowPriceCorrectionBelowTargetGain = true
; the frequency of each solution cycle (provided that the previous cycle has already completed and new data is arrived)
SolveEveryNSeconds = 15
Look at the line:
FridayExchanges = Bittrex| Binance | Kraken
it receives more than one exchange as a string separated by a pipe (|). You may choose other separators.
I suppose you parse the ini file with the support of some third party code. I used ini-parser (https://github.com/rickyah/ini-parser) that helped me a lot.
Parsing is done this way:
private static (int, string[]) ReadFridayExchanges()
{
string? configEntry = MondayConfiguration.MondaySettings["Friday"]["FridayExchanges"];
List<string> fridayExchanges = configEntry.ParseExchangeKey();
return (fridayExchanges.Count, fridayExchanges.ToArray());
}
Leave apart all the variables and names, the interesting one is MondaySettings. It is defined like this:
using IniParser;
using IniParser.Model;
/// <summary>
/// Holds the dictionary of Settings in IniData format.
/// </summary>
public static IniData MondaySettings { get; private set; }
and initialized like this:
[MemberNotNull(nameof(MondaySettings))]
private static void ReadSettingsFile(string settingsFile)
{
var iniParser = new FileIniDataParser();
MondaySettings = iniParser.ReadFile(settingsFile);
if (!DateTime.TryParse(MondaySettings["General"]["LastShutDown"], out _))
{
CorrectLastShutDownTime();
}
}
you find all nice calls in the ini-parser package to read and write the ini files automatically with almost one-liners.
When saving the file before closing and exiting:
private static bool SaveInternalSettings(string settingsFilename)
{
string settingsFile = Path.Combine(SettingsLocationFullPath, settingsFilename);
MondaySettings["General"]["LastShutDown"] = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
File.Delete(settingsFile);
try
{
var parser = new FileIniDataParser();
parser.WriteFile(settingsFile, MondaySettings);
return true;
}
catch
{
return false;
}
}
Now the interesting part: parsing multiple rhs values within ini. My solution was to do it manually, which is also one of the fastest ways.
/// <summary>
/// Reading Settings.ini for multi value lines, this routine parses the right hand side
/// of each records to define the list of values coupling it with the required
/// exchanges. Returns an array of exchanges that may be contained in a complex key to
/// resolve the All case and the presence of the pipe symbol (|).
/// NOTE: it makes use of the indexes for markets, therefore those structures should be
/// ready before this call to the method.
/// </summary>
/// <param name="rhrMarket">
/// The original key as written in the .ini file.
/// </param>
/// <returns>
/// A list of strings representing all the exchanges in a single record of
/// InterestingMarkets section the settings. If only one is contained in the original
/// key, a single element is contained.
/// </returns>
public static List<string> ParseExchangeKey(this string rhrMarket)
{
List<string> answer = new();
if (string.Equals(rhrMarket, "all", StringComparison.OrdinalIgnoreCase))
{
// producing all known exchanges as answer
foreach (KeyValuePair<string, int> ex in Tags.Exchanges.ActiveExchanges)
{
if (ex.Value != 0) // to exclude Monday which is id=0
{
answer.Add(ex.Key);
}
}
}
else if (rhrMarket.Contains('|'))
{
// sorting multiple cases
string[] split = rhrMarket.Split('|');
foreach (string? subElement in split)
{
answer.Add(subElement.ToLower().Trim());
}
}
else
{
answer.Add(rhrMarket.ToLower());
}
return answer;
}
There is also some XML to help you get to the point. As you can see, ParseExchangeKey returns a list of strings.
Just to make sure you have all the elements to get the Ienumerables, this is the definition of Tags.Exchanges.ActiveExchanges:
/// <summary>
/// Contains the set of active exchanges included into [ActiveExchanges] section in
/// settings.ini and having proper credentials to activate the web account. The
/// Values of the dictionary are the ID in _allExchanges. Monday is not included.
/// </summary>
[PublicAPI]
public static Dictionary<string, int> ActiveExchanges => _b_activeExchanges_;
/// <summary>
/// IMPL: This is internal because it needs to be set by <see cref="MondayConfiguration"/>.
/// it does not contain Monday's entry.
/// </summary>
private static readonly Dictionary<string, int> _b_activeExchanges_ = LoadExchangesDictionary();
private static Dictionary<string, int> LoadExchangesDictionary()
{
Dictionary<string, int> answer = new();
// set it internally without using the property which blocks the caller up the
// closure of the dictionary.
var counter = 0;
foreach (KeyValuePair<string, int> item in _allExchanges_)
{
if (item.Value != 0 && MondayConfiguration.Credentials.Any(c => c.Exchange == item.Key))
{
answer.Add(item.Key.ToLower(), item.Value);
counter++;
}
}
Saving is the same, however ini-parser leaves the original string with separators (pipes in this case) in its memory, if your used modified the set of multi-values, you just need to provide a simple ToString() version to concatenate it back before saving the file using the accustomed separator.
Then coupling with the controls of the Windows Forms is easy when you can get the List<string> object to move around.
This way you may skip all the custom classes for converting the type around which will likely slow down maintenance when you need to add or remove records from the ini file.
I have a public enum like so:
public enum occupancyTimeline
{
TwelveMonths,
FourteenMonths,
SixteenMonths,
EighteenMonths
}
which I will be using for a DropDown menu like so:
#Html.DropDownListFor(model => model.occupancyTimeline,
new SelectList(Enum.GetValues(typeof(CentralParkLCPreview.Models.occupancyTimeline))), "")
Now I am looking for away to have my values like so
12 Months, 14 Months, 16 Months, 18 Months instead of TweleveMonths, FourteenMonths, SixteenMonths, EighteenMonths
How would I accomplish this?
I've made myself an extension method, which I now use in every project:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Web.Mvc;
namespace App.Extensions
{
public static class EnumExtensions
{
public static SelectList ToSelectList(Type enumType)
{
return new SelectList(ToSelectListItems(enumType));
}
public static List<SelectListItem> ToSelectListItems(Type enumType, Func<object, bool> itemSelectedAction = null)
{
var arr = Enum.GetValues(enumType);
return (from object item in arr
select new SelectListItem
{
Text = ((Enum)item).GetDescriptionEx(typeof(MyResources)),
Value = ((int)item).ToString(),
Selected = itemSelectedAction != null && itemSelectedAction(item)
}).ToList();
}
public static string GetDescriptionEx(this Enum #this)
{
return GetDescriptionEx(#this, null);
}
public static string GetDescriptionEx(this Enum #this, Type resObjectType)
{
// If no DescriptionAttribute is present, set string with following name
// "Enum_<EnumType>_<EnumValue>" to be the default value.
// Could also make some code to load value from resource.
var defaultResult = $"Enum_{#this.GetType().Name}_{#this}";
var fi = #this.GetType().GetField(#this.ToString());
if (fi == null)
return defaultResult;
var customAttributes = fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (customAttributes.Length <= 0 || customAttributes.IsNot<DescriptionAttribute[]>())
{
if (resObjectType == null)
return defaultResult;
var res = GetFromResource(defaultResult, resObjectType);
return res ?? defaultResult;
}
var attributes = (DescriptionAttribute[])customAttributes;
var result = attributes.Length > 0 ? attributes[0].Description : defaultResult;
return result ?? defaultResult;
}
public static string GetFromResource(string defaultResult, Type resObjectType)
{
var searchingPropName = defaultResult;
var props = resObjectType.GetProperties();
var prop = props.FirstOrDefault(t => t.Name.Equals(searchingPropName, StringComparison.InvariantCultureIgnoreCase));
if (prop == null)
return defaultResult;
var res = prop.GetValue(resObjectType) as string;
return res;
}
public static bool IsNot<T>(this object #this)
{
return !(#this is T);
}
}
}
And then use it like this (in a View.cshtml, for example) (code is broken in two lines for clarity; could also make oneliner):
// A SelectList without default value selected
var list1 = EnumExtensions.ToSelectListItems(typeof(occupancyTimeline));
#Html.DropDownListFor(model => model.occupancyTimeline, new SelectList(list1), "")
// A SelectList with default value selected if equals "DesiredValue"
// Selection is determined by lambda expression as a second parameter to
// ToSelectListItems method which returns bool.
var list2 = EnumExtensions.ToSelectListItems(typeof(occupancyTimeline), item => (occupancyTimeline)item == occupancyTimeline.DesiredValue));
#Html.DropDownListFor(model => model.occupancyTimeline, new SelectList(list2), "")
Update
Based on Phil's suggestion, I've updated above code with possibility to read enum's display value from some resource (if you have any). Name of item in a resource should be in a form of Enum_<EnumType>_<EnumValue> (e.g. Enum_occupancyTimeline_TwelveMonths). This way you can provide text for your enum values in resource file without decorating your enum values with some attributes. Type of resource (MyResource) is included directly inside ToSelectItems method. You could extract it as a parameter of an extension method.
The other way of naming enum values is applying Description attribute (this works without adapting the code to the changes I've made). For example:
public enum occupancyTimeline
{
[Description("12 Months")]
TwelveMonths,
[Description("14 Months")]
FourteenMonths,
[Description("16 Months")]
SixteenMonths,
[Description("18 Months")]
EighteenMonths
}
You might check this link
his solution was targeting the Asp.NET , but by easy modification you can use it in MVC like
/// <span class="code-SummaryComment"><summary></span>
/// Provides a description for an enumerated type.
/// <span class="code-SummaryComment"></summary></span>
[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field,
AllowMultiple = false)]
public sealed class EnumDescriptionAttribute : Attribute
{
private string description;
/// <span class="code-SummaryComment"><summary></span>
/// Gets the description stored in this attribute.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><value>The description stored in the attribute.</value></span>
public string Description
{
get
{
return this.description;
}
}
/// <span class="code-SummaryComment"><summary></span>
/// Initializes a new instance of the
/// <span class="code-SummaryComment"><see cref="EnumDescriptionAttribute"/> class.</span>
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="description">The description to store in this attribute.</span>
/// <span class="code-SummaryComment"></param></span>
public EnumDescriptionAttribute(string description)
: base()
{
this.description = description;
}
}
the helper that will allow you to build a list of Key and Value
/// <span class="code-SummaryComment"><summary></span>
/// Provides a static utility object of methods and properties to interact
/// with enumerated types.
/// <span class="code-SummaryComment"></summary></span>
public static class EnumHelper
{
/// <span class="code-SummaryComment"><summary></span>
/// Gets the <span class="code-SummaryComment"><see cref="DescriptionAttribute" /> of an <see cref="Enum" /></span>
/// type value.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="value">The <see cref="Enum" /> type value.</param></span>
/// <span class="code-SummaryComment"><returns>A string containing the text of the</span>
/// <span class="code-SummaryComment"><see cref="DescriptionAttribute"/>.</returns></span>
public static string GetDescription(Enum value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
string description = value.ToString();
FieldInfo fieldInfo = value.GetType().GetField(description);
EnumDescriptionAttribute[] attributes =
(EnumDescriptionAttribute[])
fieldInfo.GetCustomAttributes(typeof(EnumDescriptionAttribute), false);
if (attributes != null && attributes.Length > 0)
{
description = attributes[0].Description;
}
return description;
}
/// <span class="code-SummaryComment"><summary></span>
/// Converts the <span class="code-SummaryComment"><see cref="Enum" /> type to an <see cref="IList" /> </span>
/// compatible object.
/// <span class="code-SummaryComment"></summary></span>
/// <span class="code-SummaryComment"><param name="type">The <see cref="Enum"/> type.</param></span>
/// <span class="code-SummaryComment"><returns>An <see cref="IList"/> containing the enumerated</span>
/// type value and description.<span class="code-SummaryComment"></returns></span>
public static IList ToList(Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
ArrayList list = new ArrayList();
Array enumValues = Enum.GetValues(type);
foreach (Enum value in enumValues)
{
list.Add(new KeyValuePair<Enum, string>(value, GetDescription(value)));
}
return list;
}
}
then you decorate your enum as
public enum occupancyTimeline
{
[EnumDescriptionAttribute ("12 months")]
TwelveMonths,
[EnumDescriptionAttribute ("14 months")]
FourteenMonths,
[EnumDescriptionAttribute ("16 months")]
SixteenMonths,
[EnumDescriptionAttribute ("18 months")]
EighteenMonths
}
you can use it in the controller to fill the drop down list as
ViewBag.occupancyTimeline =new SelectList( EnumHelper.ToList(typeof(occupancyTimeline)),"Value","Key");
and in your view, you can use the following
#Html.DropdownList("occupancyTimeline")
hope it will help you
Make extension Description for enumeration
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.ComponentModel;
public static class EnumerationExtensions
{
//This procedure gets the <Description> attribute of an enum constant, if any.
//Otherwise, it gets the string name of then enum member.
[Extension()]
public static string Description(Enum EnumConstant)
{
Reflection.FieldInfo fi = EnumConstant.GetType().GetField(EnumConstant.ToString());
DescriptionAttribute[] attr = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false);
if (attr.Length > 0) {
return attr(0).Description;
} else {
return EnumConstant.ToString();
}
}
}
You can use EnumDropDownListFor for this purpose. Here is an example of what you want. (Just don't forget to use EnumDropDownListFor you should use ASP MVC 5 and Visual Studio 2015.):
Your View:
#using System.Web.Mvc.Html
#using WebApplication2.Models
#model WebApplication2.Models.MyClass
#{
ViewBag.Title = "Index";
}
#Html.EnumDropDownListFor(model => model.occupancyTimeline)
And your model:
public enum occupancyTimeline
{
[Display(Name = "12 months")]
TwelveMonths,
[Display(Name = "14 months")]
FourteenMonths,
[Display(Name = "16 months")]
SixteenMonths,
[Display(Name = "18 months")]
EighteenMonths
}
Reference: What's New in ASP.NET MVC 5.1
public namespace WebApplication16.Controllers{
public enum occupancyTimeline:int {
TwelveMonths=12,
FourteenMonths=14,
SixteenMonths=16,
EighteenMonths=18
}
public static class MyExtensions {
public static SelectList ToSelectList(this string enumObj)
{
var values = from occupancyTimeline e in Enum.GetValues(typeof(occupancyTimeline))
select new { Id = e, Name = string.Format("{0} Months",Convert.ToInt32(e)) };
return new SelectList(values, "Id", "Name", enumObj);
}
}
}
Usage
#using WebApplication16.Controllers
#Html.DropDownListFor(model => model.occupancyTimeline,Model.occupancyTimeline.ToSelectList());
public enum occupancyTimeline
{
TwelveMonths=0,
FourteenMonths=1,
SixteenMonths=2,
EighteenMonths=3
}
public string[] enumString = {
"12 Months", "14 Months", "16 Months", "18 Months"};
string selectedEnum = enumString[(int)occupancyTimeLine.TwelveMonths];
or
public enum occupancyTimeline
{
TwelveMonths,
FourteenMonths,
SixteenMonths,
EighteenMonths
}
public string[] enumString = {
"12 Months", "14 Months", "16 Months", "18 Months"};
string selectedEnum = enumString[DropDownList.SelectedIndex];
In addition to using an attribute for description (see other answers or use Google), I often use RESX files where the key is the enum text (for ex. TwelveMonths) and the value is the desired text.
Then it is relatively trivial to do a function that would return the desired text and it is also easy to translate values for multilingual applications.
I also like to add some unit tests to ensure that all values have an associated text. If there are some exceptions (like maybe a Count value at the end, then the unit test would exclude those from the check.
All that stuff is not really hard and quite flexible. If you use DescriptionAttribute and want multilingual support, you need to use resource files anyway.
Usually, I would have one class to get values (displayed name) per enum type that need to be displayed and one unit test file per resource file although I have some other classes for some common code like comparing key in resource file with value in enum for unit test (and one overload allows to specify exception).
By the way, in a case like that, it could make sense to have the value of an enum matches the number of months (for ex. TwelveMonths = 12). In such case, you can also use string.Format for displayed values and also have exceptions in resource (like singular).
MVC 5.1
#Html.EnumDropDownListFor(model => model.MyEnum)
MVC 5
#Html.DropDownList("MyType",
EnumHelper.GetSelectList(typeof(MyType)) ,
"Select My Type",
new { #class = "form-control" })
MVC 4
You can refer this link Create a dropdown list from an Enum in Asp.Net MVC
Posting template for your solution, you may change some parts according your needs.
Define generic EnumHelper for formating enums:
public abstract class EnumHelper<T> where T : struct
{
static T[] _valuesCache = (T[])Enum.GetValues(typeof(T));
public virtual string GetEnumName()
{
return GetType().Name;
}
public static T[] GetValuesS()
{
return _valuesCache;
}
public T[] GetValues()
{
return _valuesCache;
}
virtual public string EnumToString(T value)
{
return value.ToString();
}
}
Define MVC generic drop down list extension helper:
public static class SystemExt
{
public static MvcHtmlString DropDownListT<T>(this HtmlHelper htmlHelper,
string name,
EnumHelper<T> enumHelper,
string value = null,
string nonSelected = null,
IDictionary<string, object> htmlAttributes = null)
where T : struct
{
List<SelectListItem> items = new List<SelectListItem>();
if (nonSelected != null)
{
items.Add(new SelectListItem()
{
Text = nonSelected,
Selected = string.IsNullOrEmpty(value),
});
}
foreach (T item in enumHelper.GetValues())
{
if (enumHelper.EnumToIndex(item) >= 0)
items.Add(new SelectListItem()
{
Text = enumHelper.EnumToString(item),
Value = item.ToString(), //enumHelper.Unbox(item).ToString()
Selected = value == item.ToString(),
});
}
return htmlHelper.DropDownList(name, items, htmlAttributes);
}
}
Any time you need to format some Enums define EnumHelper for particular enum T:
public class OccupancyTimelineHelper : EnumHelper<OccupancyTimeline>
{
public override string EnumToString(occupancyTimeline value)
{
switch (value)
{
case OccupancyTimelineHelper.TwelveMonths:
return "12 Month";
case OccupancyTimelineHelper.FourteenMonths:
return "14 Month";
case OccupancyTimelineHelper.SixteenMonths:
return "16 Month";
case OccupancyTimelineHelper.EighteenMonths:
return "18 Month";
default:
return base.EnumToString(value);
}
}
}
Finally use the code in View:
#Html.DropDownListT("occupancyTimeline", new OccupancyTimelineHelper())
I'd go for a slightly different and perhaps simpler approach:
public static List<int> PermittedMonths = new List<int>{12, 14, 16, 18};
Then simply:
foreach(var permittedMonth in PermittedMonths)
{
MyDropDownList.Items.Add(permittedMonth.ToString(), permittedMonth + " months");
}
Using enums in the way you describe I feel can be a bit of a trap.
You could use a getter casted into an integer, instead of trying to draw a property in a view from an Enum. My problem was that I was writing the value of enum as string, but I would like to have its int value.
For example you could have the below two properties in a View Model class:
public enum SomeEnum
{
Test = 0,
Test1 = 1,
Test2,
}
public SomeEnum SomeEnum {get; init;}
public int SomeEnumId => (int) SomeEnum;
Then you could access your enum through a razor View as such:
<input type="hidden" asp-for="#Model.SomeEntityId" />
I have the following code:
[Serializable]
public class CustomClass
{
public CustomClass()
{
this.Init();
}
public void Init()
{
foreach (PropertyInfo p in this.GetType().GetProperties())
{
DescriptionAttribute da = null;
DefaultValueAttribute dv = null;
foreach (Attribute attr in p.GetCustomAttributes(true))
{
if (attr is DescriptionAttribute)
{
da = (DescriptionAttribute) attr;
}
if (attr is DefaultValueAttribute)
{
dv = (DefaultValueAttribute) attr;
}
}
UInt32 value = 0;
if (da != null && !String.IsNullOrEmpty(da.Description))
{
value = Factory.Instance.SelectByCode(da.Description, 3);
}
if (dv != null && value == 0)
{
value = (UInt32) dv.Value;
}
p.SetValue(this, value, null);
}
}
private UInt32 name;
[Description("name")]
[DefaultValue(41)]
public UInt32 Name
{
get { return this.name; }
set { this.name = value; }
}
(30 more properties)
}
Now the weird thing is: when I try to serialize this class I will get an empty node CustomClass!
<CustomClass />
And when I remove Init from the constructor it works as expected! I will get the full xml representation of the class but ofcourse without values (all with value 0).
<CustomClass>
<Name>0</Name>
...
</CustomClass>
Also, when I comment out the body of Init, I will get the same as above (the one with default values)
I've tried it with a public method, with a Helper class everything, but it does not work. That is, instead of the expected:
<CustomClass>
<Name>15</Name>
...
</CustomClass>
I will get
<CustomClass />
It seems when I use reflection in this class, serialization is not possible.
Or to summarize: when I call Init or when I fill my properties with reflection -> Serialization fails, when I remove this code part -> Serialization works but of course without my values.
Is this true? And does somebody know an alternative for my solution?
It should automatically get something from the database based on the Description and when this returns nothing it falls back to the DefaultValue...
PS1: I am using the XmlSerializer
PS2: When I set a breakpoint before the serialization, I can see that all the properties are filled with the good values (like 71, 72 etc).
Now the weird thing is: when I try to serialize this class I will get an empty node CustomClass!
XmlSerializer uses DefaultValue to decide which values to serialize - if it matches the default value, it doesn't store it. This approach is consistent with similar models such as data-binding / model-binding.
Frankly, I would say that in this case both DefaultValueAttribute and DescriptionAttribute are poor choices. Write your own - perhaps EavInitAttribute - then use something like:
[EavInit(41, "name")]
public uint Name {get;set;}
Note that there are other ways of controlling this conditional serialization - you could write a method like:
public bool ShouldSerializeName() { return true; }
which will also work to convince it to write the value (this is another pattern recognised by various serialization and data-binding APIs) - but frankly this is even more work (it is per-property, and needs to be public, so it makes a mess of the API).
Finally, I would say that hitting the database multiple times (once per property) for every new object construction is very expensive - especially since many of those values are likely to be assigned values in a moment anyway (so looking them up is wasted effort). I would put a lot of thought into making this both "lazy" and "cached" if it was me.
An example of a lazy and "sparse" implementation:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
static class Program
{
static void Main()
{
var obj = new CustomClass();
Console.WriteLine(obj.Name);
// show it working via XmlSerializer
new XmlSerializer(obj.GetType()).Serialize(Console.Out, obj);
}
}
public class CustomClass : EavBase
{
[EavInit(42, "name")]
public uint Name
{
get { return GetEav(); }
set { SetEav(value); }
}
}
public abstract class EavBase
{
private Dictionary<string, uint> values;
protected uint GetEav([CallerMemberName] string propertyName = null)
{
if (values == null) values = new Dictionary<string, uint>();
uint value;
if (!values.TryGetValue(propertyName, out value))
{
value = 0;
var prop = GetType().GetProperty(propertyName);
if (prop != null)
{
var attrib = (EavInitAttribute)Attribute.GetCustomAttribute(
prop, typeof(EavInitAttribute));
if (attrib != null)
{
value = attrib.DefaultValue;
if (!string.IsNullOrEmpty(attrib.Key))
{
value = LookupDefaultValueFromDatabase(attrib.Key);
}
}
}
values.Add(propertyName, value);
}
return value;
}
protected void SetEav(uint value, [CallerMemberName] string propertyName = null)
{
(values ?? (values = new Dictionary<string, uint>()))[propertyName] = value;
}
private static uint LookupDefaultValueFromDatabase(string key)
{
// TODO: real code here
switch (key)
{
case "name":
return 7;
default:
return 234;
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
protected class EavInitAttribute : Attribute
{
public uint DefaultValue { get; private set; }
public string Key { get; private set; }
public EavInitAttribute(uint defaultValue) : this(defaultValue, "") { }
public EavInitAttribute(string key) : this(0, key) { }
public EavInitAttribute(uint defaultValue, string key)
{
DefaultValue = defaultValue;
Key = key ?? "";
}
}
}
I have this (simplified) class:
public class StarBuildParams
{
public int BaseNo { get; set; }
public int Width { get; set; }
}
And I have to transform instances of it to a querystring like this:
"BaseNo=5&Width=100"
Additionally I have to transform such a querystring back in an object of that class.
I know that this is pretty much what a modelbinder does, but I don't have the controller context in my situation (some deep buried class running in a thread).
So, is there a simple way to convert a object in a query string and back without having a controller context?
It would be great to use the modelbinding but I don't know how.
A solution with Newtonsoft Json serializer and linq:
string responseString = "BaseNo=5&Width=100";
var dict = HttpUtility.ParseQueryString(responseString);
string json = JsonConvert.SerializeObject(dict.Cast<string>().ToDictionary(k => k, v => dict[v]));
StarBuildParams respObj = JsonConvert.DeserializeObject<StarBuildParams>(json);
You can use reflection, something like this:
public T GetFromQueryString<T>() where T : new(){
var obj = new T();
var properties = typeof(T).GetProperties();
foreach(var property in properties){
var valueAsString = HttpContext.Current.Request.QueryString[property.PropertyName];
var value = Parse( valueAsString, property.PropertyType);
if(value == null)
continue;
property.SetValue(obj, value, null);
}
return obj;
}
You'll need to implement the Parse method, just using int.Parse, decimal.Parse, DateTime.Parse, etc.
Use this Parse method with the ivowiblo's solution (accepted answer):
public object Parse(string valueToConvert, Type dataType)
{
TypeConverter obj = TypeDescriptor.GetConverter(dataType);
object value = obj.ConvertFromString(null, CultureInfo.InvariantCulture, valueToConvert);
return value;
}
You can set the properties of this object in its constructor by retrieving the relevant values from the querystring
public StarBuildParams()
{
this.BaseNo = Int32.Parse(Request.QueryString["BaseNo"].ToString());
this.Width = Int32.Parse(Request.QueryString["Width"].ToString());
}
and you can ensure that the object is converted to the correct querystring format by overriding the ToString method.
public override string ToString()
{
return String.Format("BaseNo={0}&Width={1}", this.BaseNo, this.Width);
}
You'll still need to construct and call ToString in the appropriate places, but this should help.
You can just use .NET's HttpUtility.ParseQueryString() method:
HttpUtility.ParseQueryString("a=b&c=d") produces a NameValueCollection as such:
[0] Key = "a", Value = "b"
[1] Key = "c", Value = "d"
This should work so long as none of the properties match any other route parameters like controller, action, id, etc.
new RouteValueDictionary(Model)
http://msdn.microsoft.com/en-us/library/cc680272.aspx
Initializes a new instance of the RouteValueDictionary class and adds
values that are based on properties from the specified object.
To parse back from the query string you can use the model class as an action parameter and let the ModelBinder do it's job.
Serialize query string and deserialize to your class object
JObject json;
Request.RequestUri.TryReadQueryAsJson(out json);
string sjson = JsonConvert.SerializeObject(json);
StarBuildParams query = JsonConvert.DeserializeObject<StarBuildParams>(sjson);
Building off of Ivo and Anupam Singh's great solutions above, here is the code that I used to turn this into a base class for POST requests (in the event that you may only have the raw query string like in a Web API setup). This code works for lists of objects, but could easily be modified to parse a single object.
public class PostOBjectBase
{
/// <summary>
/// Returns a List of List<string> - one for each object that is going to be parsed.
/// </summary>
/// <param name="entryListString">Raw query string</param>
/// <param name="firstPropertyNameOfObjectToParseTo">The first property name of the object that is sent in the list (unless otherwise specified). Used as a key to start a new object string list. Ex: "id", etc.</param>
/// <returns></returns>
public List<List<string>> GetQueryObjectsAsStringLists(string entryListString, string firstPropertyNameOfObjectToParseTo = null)
{
// Decode the query string (if necessary)
string raw = System.Net.WebUtility.UrlDecode(entryListString);
// Split the raw query string into it's data types and values
string[] entriesRaw = raw.Split('&');
// Set the first property name if it is not provided
if (firstPropertyNameOfObjectToParseTo == null)
firstPropertyNameOfObjectToParseTo = entriesRaw[0].Split("=").First();
// Create a list from the raw query array (more easily manipulable) for me at least
List<string> rawList = new List<string>(entriesRaw);
// Initialize List of string lists to return - one list = one object
List<List<string>> entriesList = new List<List<string>>();
// Initialize List for current item to be added to in foreach loop
bool isFirstItem = false;
List<string> currentItem = new List<string>();
// Iterate through each item keying off of the firstPropertyName of the object we will ultimately parse to
foreach (string entry in rawList)
{
if (entry.Contains(firstPropertyNameOfObjectToParseTo + "="))
{
// The first item needs to be noted in the beginning and not added to the list since it is not complete
if (isFirstItem == false)
{
isFirstItem = true;
}
// Finished getting the first object - we're on the next ones in the list
else
{
entriesList.Add(currentItem);
currentItem = new List<string>();
}
}
currentItem.Add(entry);
}
// Add the last current item since we could not in the foreach loop
entriesList.Add(currentItem);
return entriesList;
}
public T GetFromQueryString<T>(List<string> queryObject) where T : new()
{
var obj = new T();
var properties = typeof(T).GetProperties();
foreach (string entry in queryObject)
{
string[] entryData = entry.Split("=");
foreach (var property in properties)
{
if (entryData[0].Contains(property.Name))
{
var value = Parse(entryData[1], property.PropertyType);
if (value == null)
continue;
property.SetValue(obj, value, null);
}
}
}
return obj;
}
public object Parse(string valueToConvert, Type dataType)
{
if (valueToConvert == "undefined" || valueToConvert == "null")
valueToConvert = null;
TypeConverter obj = TypeDescriptor.GetConverter(dataType);
object value = obj.ConvertFromString(null, CultureInfo.InvariantCulture, valueToConvert);
return value;
}
}
Then you can inherit from this class in wrapper classes for POST requests and parse to whichever objects you need. In this case, the code parses a list of objects passed as a query string to a list of wrapper class objects.
For example:
public class SampleWrapperClass : PostOBjectBase
{
public string rawQueryString { get; set; }
public List<ObjectToParseTo> entryList
{
get
{
List<List<string>> entriesList = GetQueryObjectsAsStringLists(rawQueryString);
List<ObjectToParseTo> entriesFormatted = new List<ObjectToParseTo>();
foreach (List<string> currentObject in entriesList)
{
ObjectToParseToentryPost = GetFromQueryString<ObjectToParseTo>(currentObject);
entriesFormatted.Add(entryPost);
}
return entriesFormatted;
}
}
}
I have a string property that I would like to be able to force two things with:
- It can only be set to specific vaues within a pre-defined list,
- Error checking of the property's value can be performed at compile time.
An enum fits the bill perfectly except that in my list of pre-defined strings there is one with a hyphen and enum values cannot contain hyphens. To illustrate the ideal solution if an enum could contain hyphens I would create an enum of:
public enum SIPEventPackagesEnum
{
dialog,
message-summary,
refer
}
To use:
SIPEventPackagesEnum EventPackage = SIPEventPackagesEnum.message-summary;
To set:
string eventPackageStr = "message-summary";
SIPEventPackagesEnum EventPackage = (SIPEventPackagesEnum)Enum.Parse(typeof(SIPEventPackagesEnum), eventPackageStr, true);
In the above cases it's impossible to set the EventPackage property to anything but one of the enum values and is intuitive to use since intellisense will list the available options.
Due to the inability to use a hyphen, and I cannot change the pre-defined list to remove the hyphen, the very crude approach is to use a struct and have a "Value" property on the struct that does the enforcing in its setter, see below. It's very verbose compared to using an enum and also doesn't allow any compile time checking and isn't very intuitive.
Has anyone encountered this problem before and have a better solution? I have multiple lists with items containing hyphens so it's not a once off.
public struct SIPEventPackages
{
public const string DIALOG = "dialog";
public const string MESSAGE_SUMMARY = "message-summary";
public const string REFER = "refer";
public string Value
{
get { return Value; }
set
{
if (IsValid(value))
{
Value = value.ToLower();
}
else
{
throw new ArgumentException(value + " is invalid for a SIP event package.");
}
}
}
public bool IsValid(string value)
{
if (value.IsNullOrBlank())
{
return false;
}
else if (value.ToLower() == DIALOG || value.ToLower() == MESSAGE_SUMMARY || value.ToLower() == REFER)
{
return true;
}
else
{
return false;
}
}
public override string ToString()
{
return Value;
}
}
Seems this is simple using an delegate
Func<string, bool> isValid = str =>
{
List<string> validLst = new List<string>() { "dialog","message-summary","refer" };
if (validLst.Find(x => string.Equals(x,str,StringComparison.InvariantCultureIgnoreCase)) == null)
return false;
return true;
};
var teststr1 = "message-summary";
var teststr2 = "wrongone";
isValid(teststr1);
isValid(teststr2);
Uptdate:
Otherwise you can use the enum approach in a little different way. Have a enum value without an hyphen. And just strip the hyphens from your source string when parse the enum values. this will work as you expected
public enum SIPEventPackagesEnum
{
dialog,
messagesummary,
refer
}
string eventPackageStr = "message-summary";
SIPEventPackagesEnum EventPackage = (SIPEventPackagesEnum)Enum.Parse(typeof(SIPEventPackagesEnum), eventPackageStr.Replace("-",""), true);
You could create the enum without -, then have a static Dictionary<SIPEventPackagesEnum
,string> in a helper class mapping from enum value to string to convert from enum to string, then use Enum.Parse(typeof(SIPEventPackagesEnum), str.Replace("-", "")) when converting from string to enum.
Or use _ instead of - and replace _ with - and vice versa when required
Or use camel case in the enum values and replace a capital letter within the enum name with "-<lowercase letter>" using a regex
I managed to fine tune my approach so that it's almost as good as an enum albeit with a lot more plumbing code required. It's worth the plumbing code to save potential misues problems in the future.
public struct SIPEventPackage
{
public static SIPEventPackage None = new SIPEventPackage(null);
public static SIPEventPackage Dialog = new SIPEventPackage("dialog");
public static SIPEventPackage MessageSummary = new SIPEventPackage("message-summary");
public static SIPEventPackage Refer = new SIPEventPackage("refer");
private string m_value;
private SIPEventPackage(string value)
{
m_value = value;
}
public override string ToString()
{
return m_value;
}
public static SIPEventPackage Parse(string value)
{
if (!IsValid(value))
{
throw new ArgumentException("The value is not valid for a SIPEventPackage.");
}
else
{
string trimmedValue = value.Trim().ToLower();
switch (trimmedValue)
{
case "dialog": return SIPEventPackage.Dialog;
case "message-summary": return SIPEventPackage.MessageSummary;
case "refer": return SIPEventPackage.Refer;
default: throw new ArgumentException("The value is not valid for a SIPEventPackage.");
}
}
}
}
There's a little bit more plumbing required, implementing an IsValid method and operator == and a few more, but the main thing is I can now use the struct in almost an identical way to an enum and can have items with hyphens.