使用微软提供的Settings以及自定义SettingsProvider

前言

在程序中,难免会用到配置文件。如何处理配置文件问题,有许许多多的解决方案。简单的就是直接在app.config里面加一条,然后读取。复杂一些的,可能需要自己去定义格式,存储位置。微软为我们提供了一个方案,就是在项目中的Settings

Settings

使用Settings

Settings为我们提供了设置界面,方便操作。使用起来非常简单。
如果你新建一个WPF项目,模板中其实默认就带有一个Settings文件。就像上一节中的插图一样,展开Proerpties即可见。你也可以像添加一般的类一样添加新的Settings文件,只需要在添加文件的窗口中找到Settings类型,添加即可。

添加新的Settings文件

双击Settings文件,进入可视化编辑界面。
可视化编辑界面

类型选择

作用范围选择

第一列是设置项的名称。该值作为将来在程序中获取或设置此项设置时的键。
第二列是设置项的类型。默认有很多的基本类型,注意最有一项,是可以选择自定义类型的。但是也要支持序列化的类型才行,如果是很复杂的类型,需要对其添加序列化和反序列化的方法。
第三项是设置项的作用范围。只有两种选择,Application和User。如果你选择Application,那么该设置项的值不能被修改。如果你选择User,该设置项可以被修改,但是仅针对于当前计算机用户生效。
第四项是设置项的默认值。这个没什么好说的,记得默认值要和类型匹配,不然会编译不通过。

添加完成后,记得点一下保存。VS会帮你生成对应的代码。对应的代码你可以在Settings文件对应的cs文件中找到。

生成的代码位置

单项设置对应的代码:

        [global::System.Configuration.UserScopedSettingAttribute()]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [global::System.Configuration.DefaultSettingValueAttribute("123")]
        public string Setting {
            get {
                return ((string)(this["Setting"]));
            }
            set {
                this["Setting"] = value;
            }
        }

截图中的设置生成的对应代码:

    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
    internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
        
        private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
        
        public static Settings Default {
            get {
                return defaultInstance;
            }
        }
        
        [global::System.Configuration.UserScopedSettingAttribute()]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        [global::System.Configuration.DefaultSettingValueAttribute("123")]
        public string Setting {
            get {
                return ((string)(this["Setting"]));
            }
            set {
                this["Setting"] = value;
            }
        }
    }

在CS中使用设置就更简单了。

        static void Main(string[] args)
        {
            //读取设置
            var t = Properties.Settings.Default.Setting;
            Console.WriteLine(t);
            //更新设置
            Properties.Settings.Default.Setting = "6666666";
            //保存设置
            Properties.Settings.Default.Save();
        }

如何使用到此结束。

工作流程及原理简述

  1. 可视化界面添加需要的设置项。保存时会生成对应的cs文件,同时,会有对应的xml内容写入app.config。
  2. 执行到读取设置时,会根据设置项的作用范围,去读取不同的配置文件。如果没有读取到值,会返回默认值。执行到写入设置时,由于只有User的类型才能写入,系统会调用默认的LocalFileSettingsProvider保存到当前用户的AppData\Local\{ApplicationName}\{Version}\{ApplicationName+LocationHashValue}。

原理其实非常简单。每次读或写都生成一个SettingsProvider的实例,然后通过这个实例进行读或者写。你可以通过添加属性来指定SettingsProvider的类型。默认会使用LocalFileSettingsProvider。由于LocalFileSettingsProvider没有提供修改保存路径的方法,我们需要自定义SettingsProvider来修改保存路径。需要将[SettingsProvider(typeof(CustomProvider))]添加到生成的cs类上。

微软表示不提供路径修改是出于安全考虑。具体的内容请自行参阅官方文档。

自定义SettingsProvider

直接提供给大家一个可以用的。讲解我放在后面。这个类是拔微软源码做的。

    [
         PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust"),
         PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")
    ]
    public class CustomSettingsProvider : SettingsProvider
    {
        private const string UserSettingsGroupName = "userSettings";
        private string _applicationName;
        public override string ApplicationName { get => _applicationName; set => _applicationName = value; }

        public override void Initialize(string name, NameValueCollection values)
        {
            if (String.IsNullOrEmpty(name))
            {
                name = "CustomProvider";
            }

            base.Initialize(name, values);
        }

        private Configuration configuration;

        private void Open()
        {
            var fileMap = new ExeConfigurationFileMap
            {
                ExeConfigFilename = $"{_applicationName}.exe.config",
                RoamingUserConfigFilename = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\" + _applicationName + "\\Settings\\user.config"
            };
            configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.PerUserRoaming);
        }

        [
         FileIOPermission(SecurityAction.Assert, AllFiles = FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read),
         PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust"),
         PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")
        ]
        public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)
        {
            Open();
            var settings = ReadSettingsFromFile(GetSectionName(context));
            var values = new SettingsPropertyValueCollection();
            foreach (SettingsProperty settingProperty in collection)
            {
                var value = new SettingsPropertyValue(settingProperty);
                if (settings.Contains(settingProperty.Name))
                {
                    var ss = (StoredSetting)settings[settingProperty.Name];
                    var valueString = ss.xmlNode.InnerXml;
                    if (ss.serializeAs == SettingsSerializeAs.String)
                    {
                        valueString = Escaper.Unescape(valueString);
                    }
                    value.SerializedValue = valueString;
                }
                else if (settingProperty.DefaultValue != null)
                {
                    value.SerializedValue = settingProperty.DefaultValue;
                }

                value.IsDirty = false;
                values.Add(value);
            }
            return values;
        }

        private XmlEscaper Escaper = new XmlEscaper();

        private IDictionary ReadSettingsFromFile(string sectionName)
        {
            IDictionary settings = new Hashtable();

            var sectionGroup = configuration.GetSectionGroup(UserSettingsGroupName);
            var section = sectionGroup.Sections[sectionName] as ClientSettingsSection;
            if (section != null)
            {
                foreach (SettingElement setting in section.Settings)
                {
                    settings[setting.Name] = new StoredSetting(setting.SerializeAs, setting.Value.ValueXml);
                }
            }

            return settings;
        }

        private string GetSectionName(SettingsContext context)
        {
            string groupName = (string)context["GroupName"];
            string key = (string)context["SettingsKey"];

            Debug.Assert(groupName != null, "SettingsContext did not have a GroupName!");

            string sectionName = groupName;

            if (!String.IsNullOrEmpty(key))
            {
                sectionName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", sectionName, key);
            }

            return XmlConvert.EncodeLocalName(sectionName);
        }

        public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)
        {
            string sectionName = GetSectionName(context);
            IDictionary userSettings = new Hashtable();
            foreach (SettingsPropertyValue value in collection)
            {
                SettingsProperty setting = value.Property;
                if (value.IsDirty)
                {
                    StoredSetting ss = new StoredSetting(setting.SerializeAs, SerializeToXmlElement(setting, value));
                    userSettings[setting.Name] = ss;
                }
            }
            WriteSettings(sectionName, userSettings);
        }

        private void WriteSettings(string sectionName, IDictionary newSettings)
        {
            Open();
            var section = GetConfigSection(sectionName);

            if (section != null)
            {
                SettingElementCollection sec = section.Settings;
                foreach (DictionaryEntry entry in newSettings)
                {
                    SettingElement se = sec.Get((string)entry.Key);

                    if (se == null)
                    {
                        se = new SettingElement();
                        se.Name = (string)entry.Key;
                        sec.Add(se);
                    }

                    StoredSetting ss = (StoredSetting)entry.Value;
                    se.SerializeAs = ss.serializeAs;
                    se.Value.ValueXml = ss.xmlNode;
                }

                try
                {
                    configuration.Save();
                }
                catch (ConfigurationErrorsException ex)
                {
                    // We wrap this in an exception with our error message and throw again.
                    throw new ConfigurationErrorsException($"Save file to {configuration.FilePath} failed", ex);
                }
            }
            else
            {
                throw new ConfigurationErrorsException($"Can not find the section {section} in the setting file");
            }
        }

        private ClientSettingsSection GetConfigSection(string sectionName)
        {
            Configuration config = configuration;
            string fullSectionName = UserSettingsGroupName + "/" + sectionName;
            ClientSettingsSection section = null;

            if (config != null)
            {
                section = config.GetSection(fullSectionName) as ClientSettingsSection;

                if (section == null)
                {
                    // Looks like the section isn't declared - let's declare it and try again.
                    DeclareSection(sectionName);
                    section = config.GetSection(fullSectionName) as ClientSettingsSection;
                }
            }

            return section;
        }

        // Declares the section handler of a given section in its section group, if a declaration isn't already
        // present. 
        private void DeclareSection(string sectionName)
        {
            Configuration config = configuration;
            ConfigurationSectionGroup settingsGroup = config.GetSectionGroup(UserSettingsGroupName);

            if (settingsGroup == null)
            {
                //Declare settings group
                ConfigurationSectionGroup group = new UserSettingsGroup();
                config.SectionGroups.Add(UserSettingsGroupName, group);
            }

            settingsGroup = config.GetSectionGroup(UserSettingsGroupName);

            Debug.Assert(settingsGroup != null, "Failed to declare settings group");

            if (settingsGroup != null)
            {
                ConfigurationSection section = settingsGroup.Sections[sectionName];
                if (section == null)
                {
                    section = new ClientSettingsSection();
                    section.SectionInformation.AllowExeDefinition = ConfigurationAllowExeDefinition.MachineToLocalUser;
                    section.SectionInformation.RequirePermission = false;
                    settingsGroup.Sections.Add(sectionName, section);
                }
            }
        }

        private XmlNode SerializeToXmlElement(SettingsProperty setting, SettingsPropertyValue value)
        {
            XmlDocument doc = new XmlDocument();
            XmlElement valueXml = doc.CreateElement("value");

            string serializedValue = value.SerializedValue as string;

            if (serializedValue == null && setting.SerializeAs == SettingsSerializeAs.Binary)
            {
                // SettingsPropertyValue returns a byte[] in the binary serialization case. We need to
                // encode this - we use base64 since SettingsPropertyValue understands it and we won't have
                // to special case while deserializing.
                byte[] buf = value.SerializedValue as byte[];
                if (buf != null)
                {
                    serializedValue = Convert.ToBase64String(buf);
                }
            }

            if (serializedValue == null)
            {
                serializedValue = String.Empty;
            }

            // We need to escape string serialized values
            if (setting.SerializeAs == SettingsSerializeAs.String)
            {
                serializedValue = Escaper.Escape(serializedValue);
            }

            valueXml.InnerXml = serializedValue;

            // Hack to remove the XmlDeclaration that the XmlSerializer adds. 
            XmlNode unwanted = null;
            foreach (XmlNode child in valueXml.ChildNodes)
            {
                if (child.NodeType == XmlNodeType.XmlDeclaration)
                {
                    unwanted = child;
                    break;
                }
            }
            if (unwanted != null)
            {
                valueXml.RemoveChild(unwanted);
            }

            return valueXml;
        }

        private class XmlEscaper
        {
            private XmlDocument doc;
            private XmlElement temp;

            internal XmlEscaper()
            {
                doc = new XmlDocument();
                temp = doc.CreateElement("temp");
            }

            internal string Escape(string xmlString)
            {
                if (String.IsNullOrEmpty(xmlString))
                {
                    return xmlString;
                }

                temp.InnerText = xmlString;
                return temp.InnerXml;
            }

            internal string Unescape(string escapedString)
            {
                if (String.IsNullOrEmpty(escapedString))
                {
                    return escapedString;
                }

                temp.InnerXml = escapedString;
                return temp.InnerText;
            }
        }
    }
    internal class StoredSetting
    {
        public StoredSetting(SettingsSerializeAs serializeAs, XmlNode xmlNode)
        {
            this.serializeAs = serializeAs;
            this.xmlNode = xmlNode;
        }
        internal SettingsSerializeAs serializeAs;
        internal XmlNode xmlNode;
    }

主要的地方在于继承SettingsProvider然后实现两个必须实现的方法,分别是public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection collection)public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection collection)。这两个方法会在读和写的时候被调用。之前也提到过,每次读写,都会生成一个新的SettingsProvider的实例,这点需要注意。其中,ApplicationName是在工程设置里Assmebly Information中的值。
可能解释得不是很清楚,有什么问题欢迎留言。当然,更推荐去看LocalFileSettingsProvider的源码。

你可能感兴趣的:(使用微软提供的Settings以及自定义SettingsProvider)