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('').Split('');
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);
}
}
3
Answers
If you are going to use a custom
TypeConverter
, you’ll have to register it as a provider toTypeDescriptionProvider
: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.*NOTE: In my testing, the method
ConvertFrom
is called twice, once fromconverter.IsValid
and once frompropertyInfo.SetValue
.*NOTE2: You are using
streamwriter
to update the ini file. Since you are usingGetPrivateProfileSection
, it seems you should be usingWritePrivateProfileSection
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 betweenstring
s andstring
s) to house a collection of right hand side (rhs) values. Obviously, they cannot reside of a left hand side (lhs) of theini
line. You could, but then likely the choice of aini
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, andFriday
is an app usingMonday
for trading online with a specific algo. Nothing important for the case being.The
ini
file looks like this:Look at the line:
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:
Leave apart all the variables and names, the interesting one is
MondaySettings
. It is defined like this:and initialized like this:
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:
}
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.There is also some XML to help you get to the point. As you can see,
ParseExchangeKey
returns a list ofstring
s.Just to make sure you have all the elements to get the Ienumerables, this is the definition of
Tags.Exchanges.ActiveExchanges
: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.The reason your type converter is not used is because of this line:
You are getting the TypeConverter that is defined on the type of the property. So for
ReplaceItemsList
that would be the TypeConverter forList<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’sGetCustomAttributes
or you do what the PropertyGrid does which is use thePropertyDescriptor
s to get to the properties and their state. The latter would be better since if the object implementedICustomTypeDescriptor
or some other type augmentation likeTypeDescriptionProvider
then you would get that automatically.So something like the following for the Save using PropertyDescriptors would be: