前言
在程序中,难免会用到配置文件。如何处理配置文件问题,有许许多多的解决方案。简单的就是直接在app.config里面加一条,然后读取。复杂一些的,可能需要自己去定义格式,存储位置。微软为我们提供了一个方案,就是在项目中的Settings
。
使用Settings
Settings
为我们提供了设置界面,方便操作。使用起来非常简单。
如果你新建一个WPF项目,模板中其实默认就带有一个Settings
文件。就像上一节中的插图一样,展开Proerpties即可见。你也可以像添加一般的类一样添加新的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();
}
如何使用到此结束。
工作流程及原理简述
- 可视化界面添加需要的设置项。保存时会生成对应的cs文件,同时,会有对应的xml内容写入app.config。
- 执行到读取设置时,会根据设置项的作用范围,去读取不同的配置文件。如果没有读取到值,会返回默认值。执行到写入设置时,由于只有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
的源码。