如果 user.config 中有 Int32[] 属性,我在使用我的 CustomSettingsProvider 时会得到一个无效的转换异常

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

我遵循 this article 并创建了 CustomSettingsProvider 以摆脱 [=25] 路径中的“_url_somehash”部分=] 文件被存储。现在我的设置按照我的意愿存储在 \CompanyName\ProductName\Version\user.config 中。

我的 user.config 文件(在创建我的 CustomSettingsProvider 之前由我的应用程序编写)包含一个 Int32[] 属性,它由默认的 SettingsProvider 正确存储和加载。当我使用我的 CustomSettingsProvider 时,出现以下异常:

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

我该如何解决这个问题?以更一般的方式,我如何以与使用默认 SettingsProvider 相同的方式存储集合和 classes?

这是我的 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;

// ==>>>>  
// 

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

要读取序列化为 XML 的属性,您首先需要反序列化它们。

可能最简单的方法是添加一个名为 getPropertyValue 的新方法,它决定是直接 return 字符串值还是先反序列化它。然后,在下面显示的代码中,您可以调用此方法而不是使用 Convert.ChangeType 来设置 属性 值:

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

您的新方法 getPropertyValue 可能如何工作的示例:

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

这将解决无效转换错误并将您的整数数组加载到设置中。

保存设置时需要进行相应的处理。如果你遇到了并发症,我建议你 post 一个新问题,因为问题有些不同。