Search code examples
c#collectionsinipropertygrid

How to save/load a List<string> variable from PropertyGrid to an ini settings file?


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;

Visual Studio Windows Forms application with a PropertyGrid example in C#

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);
    }
}

Solution

  • 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)));
                }
            }
        }
    }