Angelo Mascaro
Angelo Mascaro

Reputation: 129

If there are Int32[] properties in user.config, I get an invalid cast exception when I use my CustomSettingsProvider

I followed this article and I created my CustomSettingsProvider in order to get rid of the "_url_somehash" part of the path where the user.config file is stored. Now my settings are stored in <LocalApplicationData>\CompanyName\ProductName\Version\user.config as I wanted.

My user.config file (written by my application before creating my CustomSettingsProvider) contains one Int32[] property that was stored and loaded correctly by the default SettingsProvider. When I use my CustomSettingsProvider I get the following exception:

Exception InvalidCastException
Source = mscorlib
Message = Invalid cast from 'System.String' to 'System.Int32[]'.
TargetSite = System.Object DefaultToType(System.IConvertible, System.Type, System.IFormatProvider)
Stack =
    System.Convert.DefaultToType(IConvertible value, Type targetType, IFormatProvider provider)
    System.String.System.IConvertible.ToType(Type type, IFormatProvider provider)
    System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
    System.Convert.ChangeType(Object value, Type conversionType)
    MyApp.Interface.CustomSettingsProvider.GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection) in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\CustomSettingsProvider.cs:line 112
    System.Configuration.SettingsBase.GetPropertiesFromProvider(SettingsProvider provider)
    System.Configuration.SettingsBase.GetPropertyValueByName(String propertyName)
    System.Configuration.SettingsBase.get_Item(String propertyName)
    System.Configuration.ApplicationSettingsBase.GetPropertyValue(String propertyName)
    System.Configuration.ApplicationSettingsBase.get_Item(String propertyName)
    MyApp.Properties.Settings.get_UpgradeRequired() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Properties\Settings.Designer.cs:line 31
    MyApp.Interface.Program.Run() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\Program.cs:line 51
    MyApp.Interface.Program.Main() in d:\Users\angelo\Documents\Visual Studio 2013\Projects\MyApp\MyApp\Interface\Program.cs:line 34

How can I fix this problem? In a more general way, how can I store collections and classes in the same way I can do it with the default SettingsProvider?

This is the full code of my CustomSettingsProvider class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Xml.Linq;
using System.IO;

// ==>>>>  https://stackoverflow.com/questions/2265271/custom-path-of-the-user-config
// https://stackoverflow.com/questions/1947185/c-sharp-get-special-folder

namespace MyApp.Interface
{
    class CustomSettingsProvider : SettingsProvider
    {
        #region Helper struct
        /// <summary>
        /// Helper struct.
        /// </summary>
        internal struct SettingStruct
        {
            internal string name;
            internal string serializeAs;
            internal string value;
        } 
        #endregion
        #region Constants
        const string NAME = "name";
        const string SERIALIZE_AS = "serializeAs";
        const string CONFIG = "configuration";
        const string USER_SETTINGS = "userSettings";
        const string SETTING = "setting"; 
        #endregion
        #region Fields
        bool _loaded; 
        #endregion
        #region Properties
        /// <summary>
        /// Override.
        /// </summary>
        public override string ApplicationName { get { return System.Reflection.Assembly.GetExecutingAssembly().ManifestModule.Name; } set { /*do nothing*/ } } 
        /// <summary>
        /// The setting key this is returning must set before the settings are used.
        /// e.g. <c>Properties.Settings.Default.SettingsKey = @"C:\temp\user.config";</c>
        /// </summary>
        private string UserConfigPath
        {
            get
            {
                System.Diagnostics.FileVersionInfo versionInfo;
                string strUserConfigPath, strUserConfigFolder;

                strUserConfigPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.Create);
                versionInfo = System.Diagnostics.FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location);
                strUserConfigPath = Path.Combine(strUserConfigPath, versionInfo.CompanyName, versionInfo.ProductName, versionInfo.ProductVersion, "user.config");
                strUserConfigFolder = Path.GetDirectoryName(strUserConfigPath);
                if(!Directory.Exists(strUserConfigFolder))
                    Directory.CreateDirectory(strUserConfigFolder);
                return strUserConfigPath;
            }
        }
        /// <summary>
        /// In memory storage of the settings values
        /// </summary>
        private Dictionary<string, SettingStruct> SettingsDictionary { get; set; }
        #endregion
        #region Constructor
        /// <summary>
        /// Loads the file into memory.
        /// </summary>
        public CustomSettingsProvider()
        {
            SettingsDictionary = new Dictionary<string, SettingStruct>();
        } 
        /// <summary>
        /// Override.
        /// </summary>
        public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
        {
            base.Initialize(ApplicationName, config);
        }
        #endregion
        /// <summary>
        /// Must override this, this is the bit that matches up the designer properties to the dictionary values
        /// </summary>
        /// <param name="context"></param>
        /// <param name="collection"></param>
        /// <returns></returns>
        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
        {
            //load the file
            if(!_loaded)
            {
                _loaded = true;
                LoadValuesFromFile();
            }
            //collection that will be returned.
            SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();
            //iterate thought the properties we get from the designer, checking to see if the setting is in the dictionary
            foreach(SettingsProperty setting in collection)
            {
                SettingsPropertyValue value = new SettingsPropertyValue(setting);
                value.IsDirty = false;

                //need the type of the value for the strong typing
                var t = Type.GetType(setting.PropertyType.FullName);

                if(SettingsDictionary.ContainsKey(setting.Name))
                {
                    value.SerializedValue = SettingsDictionary[setting.Name].value;
                    value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
                }
                else //use defaults in the case where there are no settings yet
                {
                    value.SerializedValue = setting.DefaultValue;
                    value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
                }

                values.Add(value);
            }
            return values;
        }
        /// <summary>
        /// Must override this, this is the bit that does the saving to file.  Called when Settings.Save() is called
        /// </summary>
        /// <param name="context"></param>
        /// <param name="collection"></param>
        public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
        {
            //grab the values from the collection parameter and update the values in our dictionary.
            foreach(SettingsPropertyValue value in collection)
            {
                var setting = new SettingStruct()
                {
                    value = (value.PropertyValue == null ? String.Empty : value.PropertyValue.ToString()),
                    name = value.Name,
                    serializeAs = value.Property.SerializeAs.ToString()
                };
                if(!SettingsDictionary.ContainsKey(value.Name))
                    SettingsDictionary.Add(value.Name, setting);
                else
                    SettingsDictionary[value.Name] = setting;
            }
            //now that our local dictionary is up-to-date, save it to disk.
            SaveValuesToFile();
        }
        /// <summary>
        /// Loads the values of the file into memory.
        /// </summary>
        private void LoadValuesFromFile()
        {
            string strUserConfigPath;
            strUserConfigPath = UserConfigPath;
            //if the config file is not where it's supposed to be create a new one.
            if(!File.Exists(strUserConfigPath))
                CreateEmptyConfig(strUserConfigPath);
            //System.Security.Policy.StrongName strongName = new System.Security.Policy.StrongName(
            //ClickOnce
            //load the xml
            var configXml = XDocument.Load(UserConfigPath);

            //get all of the <setting name="..." serializeAs="..."> elements.
            var settingElements = configXml.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName).Elements(SETTING);

            //iterate through, adding them to the dictionary, (checking for nulls, xml no likey nulls)
            //using "String" as default serializeAs...just in case, no real good reason.
            foreach(var element in settingElements)
            {
                var newSetting = new SettingStruct()
                {
                    name = element.Attribute(NAME) == null ? String.Empty : element.Attribute(NAME).Value,
                    serializeAs = element.Attribute(SERIALIZE_AS) == null ? "String" : element.Attribute(SERIALIZE_AS).Value,
                    value = element.Value ?? String.Empty
                };
                SettingsDictionary.Add(element.Attribute(NAME).Value, newSetting);
            }
        }
        /// <summary>
        /// Creates an empty user.config file...looks like the one MS creates.  
        /// This could be overkill a simple key/value pairing would probably do.
        /// </summary>
        private void CreateEmptyConfig(string strUserConfigPath)
        {
            Configuration config1;

            config1 = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal);
            if(File.Exists(config1.FilePath))
            {
                File.Copy(config1.FilePath, strUserConfigPath);
            }
            else
            {
                string s = Properties.Settings.Default.LastLoadedImage;
                var doc = new XDocument();
                var declaration = new XDeclaration("1.0", "utf-8", "true");
                var config = new XElement(CONFIG);
                var userSettings = new XElement(USER_SETTINGS);
                var group = new XElement(typeof(Properties.Settings).FullName);
                userSettings.Add(group);
                config.Add(userSettings);
                doc.Add(config);
                doc.Declaration = declaration;
                doc.Save(strUserConfigPath);
            }
        }
        /// <summary>
        /// Saves the in memory dictionary to the user config file
        /// </summary>
        private void SaveValuesToFile()
        {
            //load the current xml from the file.
            var import = XDocument.Load(UserConfigPath);

            //get the settings group (e.g. <Company.Project.Desktop.Settings>)
            var settingsSection = import.Element(CONFIG).Element(USER_SETTINGS).Element(typeof(Properties.Settings).FullName);

            //iterate though the dictionary, either updating the value or adding the new setting.
            foreach(var entry in SettingsDictionary)
            {
                var setting = settingsSection.Elements().FirstOrDefault(e => e.Attribute(NAME).Value == entry.Key);
                if(setting == null) //this can happen if a new setting is added via the .settings designer.
                {
                    var newSetting = new XElement(SETTING);
                    newSetting.Add(new XAttribute(NAME, entry.Value.name));
                    newSetting.Add(new XAttribute(SERIALIZE_AS, entry.Value.serializeAs));
                    newSetting.Value = (entry.Value.value ?? String.Empty);
                    settingsSection.Add(newSetting);
                }
                else //update the value if it exists.
                {
                    setting.Value = (entry.Value.value ?? String.Empty);
                }
            }
            import.Save(UserConfigPath);
        }

        #region Angelo
        private object GetDefaultValue(SettingsProperty setting)
        {
            if (setting.PropertyType.IsEnum)
                return Enum.Parse(setting.PropertyType, setting.DefaultValue.ToString());

        // Return the default value if it is set
        // Return the default value if it is set
            if (setting.DefaultValue != null)
            {
                System.ComponentModel.TypeConverter tc = System.ComponentModel.TypeDescriptor.GetConverter(setting.PropertyType);
                return tc.ConvertFromString(setting.DefaultValue.ToString());
            }
            else // If there is no default value return the default object
            {
                return Activator.CreateInstance(setting.PropertyType);
            }
        }
        #endregion
    }
}

Upvotes: 2

Views: 791

Answers (1)

Reg Edit
Reg Edit

Reputation: 6914

To read properties that are serialized as XML, you first need to deserialize them.

Probably the easiest way is to add a new method called something like getPropertyValue, which determines whether it should return the string value directly or deserialize it first. Then, in your code shown below, you can just call this method instead of using Convert.ChangeType to set property values:

var t = Type.GetType(setting.PropertyType.FullName);

if (SettingsDictionary.ContainsKey(setting.Name))
{
    value.SerializedValue = SettingsDictionary[setting.Name].value;
    // value.PropertyValue = Convert.ChangeType(SettingsDictionary[setting.Name].value, t);
    value.PropertyValue = getPropertyValue(SettingsDictionary[setting.Name].value, t, setting.SerializeAs);
}
else //use defaults in the case where there are no settings yet
{
    value.SerializedValue = setting.DefaultValue;
    //   value.PropertyValue = Convert.ChangeType(setting.DefaultValue, t);
    value.PropertyValue = getPropertyValue((string)setting.DefaultValue, t, setting.SerializeAs);
}

An example of how your new method getPropertyValue might work:

private object getPropertyValue(string settingValue, Type settingType, SettingsSerializeAs serializeAs)
{
    switch (serializeAs)
    {
        case SettingsSerializeAs.String:
            return settingValue;
        case SettingsSerializeAs.Xml:
            //for demo purposes, assumes this is your int array--otherwise do further checking to get the correct type
            XmlSerializer serializer = new XmlSerializer(typeof(int[]));
            return serializer.Deserialize(new StringReader(settingValue));
        //implement further types as required
        default:
            throw new NotImplementedException(string.Format("Settings deserialization as {0} is not implemented", serializeAs));
    }
}

This will resolve the invalid cast error and load your array of integers into the setting.

You'll need to apply the corresponding treatment when you save the settings. If you hit complications with that, I suggest you post a new question as the issues are somewhat different.

Upvotes: 1

Related Questions