使用微软提供的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的源码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 关于首选项和设置 首选项是您持久存储的信息,并用于配置您的应用程序。应用程序通常会向用户公开偏好设置,以便他们自定...
    nicedayCoco阅读 987评论 0 0
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 5,425评论 0 4
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,815评论 0 5
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,176评论 0 9