如何更改我的 .NET 应用程序的预定义 userconfig 目录?
How to change the predefined userconfig directory of my .NET application?
目前我的应用程序的用户设置存储在这个默认目录中:
C:\Users\{User Name}\AppData\Roaming\{Company Name}\{Assembly Name}.vshos_Url_{Hash}\{Assembly Version}
我知道默认的 Microsoft 命名规则的含义,我的问题是:如何在执行时或通过修改 appconfig 文件来更改默认文件夹?
我的意图是只能处理将保存我的应用程序的用户设置的目录,例如我想将用户设置文件保存在这个目录中:
C:\Users\{User Name}\AppData\Roaming\{Assembly Name}
我知道这是可以实现的,因为我见过很多 .NET 应用程序可以将其 userconfig 文件存储在自定义漫游文件夹中,该文件夹不遵循 Microsoft 默认规则以及未处理的哈希和其他烦人的规则命名规则。
我见过很多与此相关的问题,例如:
要做任何特别的事情,使用带有您自己的配置文件的 XDoc 或 LinqXML 会容易得多。
这样您就可以将它们保存在任何您喜欢的地方,并且不会遇到其他问题,例如:
存在该命名约定,以便 NET 可以确保加载了正确的设置。由于您已经将管理设置的控制权交给了 NET Framework/VB 应用程序框架,因此它还承担了确保应用加载正确设置集的责任。在这种情况下,证据散列用于唯一地识别一个 WindowsApplication1
与另一个(除其他外)。
I know this is possible to acchieve, because I've seen much .NET applications that can store its userconfig file in a custom Roaming folder
这是可能的,但我不确定一切是否与您得出的结论完全一致。我非常怀疑许多应用程序在使用自定义设置 class.
可以更轻松地将 XML 文件保存到该位置时会经历麻烦来实现自定义提供程序
简单的解决方案
自己写用户选项class,自己序列化。例如,Shared/static 方法可用于在非常少的代码中反序列化 class(这恰好使用 JSON):
Friend Shared Function Load() As UserOptions
' create instance for default on new install
Dim u As New UserOptions
If File.Exists(filePath) Then
' filepath can be anywhere you have access to!
Dim jstr = File.ReadAllText(filePath)
If String.IsNullOrEmpty(jstr) = False Then
u = JsonConvert.DeserializeObject(Of UserOptions)(jstr)
End If
End If
Return u
End Function
实现它的应用程序:
UOpt = UserOptions.Load()
在专家中,您可以完全控制文件的保存位置,并且可以使用您喜欢的任何序列化程序。最重要的是,它 简单 - 代码比下面显示的要少得多。
缺点 是使用它的代码必须手动加载和保存它们(在应用程序事件中很容易处理),并且没有花哨的设计器。
漫长而曲折的道路:自定义设置提供程序
自定义 SettingsProvider
允许您更改设置的处理、保存和加载方式,包括更改文件夹位置。
这个问题的重点是更改文件位置。问题是您的应用没有一种(干净、简单)的方式与您的 SettingsProvider
通信以指定文件夹。提供者需要能够在内部解决这个问题,当然要保持一致。
大多数人想要做的不仅仅是更改使用的文件夹名称。例如,在玩耍时,我使用了一个 SQLite 数据库来代替 XML,它反映了代码使用的结构。这使得加载本地和正确的漫游值变得非常容易。如果一直采用这种方法,代码可以大大简化,很可能整个升级过程。因此,该提供商考虑了其中一些更广泛的需求。
即使您只想更改文件名,也有 2 个关键注意事项:
本地与漫游
将提供程序编码为始终存储在 AppData\Roaming
但写入不合格的本地设置是不负责任的。区分它们是一种不应为了省略文件夹名称中的证据哈希而牺牲的功能。
注意:每个 Setting
都可以设置为 Roaming
或 Local
值:在设置编辑器中选择一个设置,打开属性窗格 - 更改 Roaming
为真。
似乎在处理自定义 SettingsProvider
将本地和漫游保存到同一个文件但在不同部分中的(非常)几个问题上达成了共识。这很有意义 - 并且比从 2 个文件加载更简单 - 所以使用的 XML 结构是:
<configuration>
<CommonShared>
<setting name="FirstRun">True</setting>
<setting name="StartTime">15:32:18</setting>
...
</CommonShared>
<MACHINENAME_A>
<setting name="MainWdwLocation">98, 480</setting>
<setting name="myGuid">d62eb904-0bb9-4897-bb86-688d974db4a6</setting>
<setting name="LastSaveFolder">C:\Folder ABC</setting>
</MACHINENAME_A>
<MACHINENAME_B>
<setting name="MainWdwLocation">187, 360</setting>
<setting name="myGuid">a1f8d5a5-f7ec-4bf9-b7b8-712e80c69d93</setting>
<setting name="LastSaveFolder">C:\Folder XYZ</setting>
</MACHINENAME_B>
</configuration>
漫游项目存储在以使用它们的机器名称命名的部分中。保留 <NameSpace>.My.MySettings
节点可能有一些价值,但我不确定它的用途是什么。
我删除了 SerializeAs
元素,因为它未被使用。
版本
如果您调用 My.Settings.Upgrade
,则不会发生任何事情。尽管它是 Settings
方法,但它实际上是 ApplicationSettingsBase
中的内容,因此您的提供者不参与。
因此,如果自动递增最后一个元素,使用完整版本字符串作为文件夹的一部分会导致问题。琐碎的重建将创建一个新文件夹并丢失和孤立旧设置。当没有当前文件时,也许您可以查找并加载以前版本的值。然后也许删除那个旧的file/folder,所以总是只有一组可能的旧设置。随意添加大量的合并代码。
为了仅更改数据存储文件夹的主要目的,我删除了版本文件夹段。使用全局提供程序时,代码会自动累积设置。已删除的设置不会“泄漏”到应用程序中,因为 NET 不会为其请求值。唯一的问题是 XML.
中会有它的值
我添加了代码来清除这些。如果您以后重复使用具有不同类型的设置名称,这将防止出现问题。例如,Foo
作为 Decimal
的旧保存值将无法与新的 Foo
作为 Size
一起使用。如果你彻底改变一种类型,事情仍然会很糟糕。不要那样做。
这个答案 Custom path of the user.config 为自定义提供程序提供了一个非常好的起点。它有一些问题并且遗漏了一些东西,但为任何提供商的一些典型步骤和样板代码提供了快速入门指南。由于许多人可能需要在此处进一步修改提供程序,因此可能值得一读(并投票)。
此处的代码借鉴了该答案的一些内容,并且:
- 添加各种改进
- 提供自定义路径
- 漫游设置检测
- 文件中的本地和漫游部分
- 正确处理复杂类型,例如
Point
或Size
- 检测并删除删除的设置
- 在VB
1。设置
在大多数情况下,您不能逐步 write/debug 这一点 - 在您完成之前几乎没有用。
- 添加对
System.Configuration
的引用
- 向您的项目添加一个新的 class
示例:
Imports System.Configuration
Public Class CustomSettingsProvider
Inherits SettingsProvider
End Class
接下来,转到设置设计器并添加一些设置以进行测试。将一些标记为漫游以进行完整测试。然后单击此处显示的 <> View Code
按钮:
人人都爱写意圈!
显然有两种方法可以实现自定义提供程序。此处的代码将使用您的代码代替 My.MySettings
。您还可以通过在“属性”窗格中键入提供程序名称来基于每个设置指定自定义提供程序,并跳过此步骤的其余部分。我没有对此进行测试,但它应该是这样工作的。
为了使用“您”编写的新设置提供程序,它需要使用属性与 MySettings
相关联:
Imports System.Configuration
<SettingsProvider(GetType(ElectroZap.CustomSettingsProvider))>
Partial Friend NotInheritable Class MySettings
End Class
'ElektroZap' 是您的根命名空间,'ElektroApp' 是您的应用程序名称。构造函数中的代码可以更改为使用产品名称或模块名称。
我们完成了那个文件;保存并关闭它。
2。设置提供者
首先,请注意此 CustomProvider 是通用的,只需将其指定为 SettingsProvider
即可与任何应用程序一起使用。但它实际上只做了两件事:
- 使用自定义路径
- 将本地和漫游设置合并到一个文件中
通常情况下,在求助于自定义提供程序之前会有更长的待办事项列表,因此对于许多人来说,这可能只是提供其他事情的起点。请记住,某些更改可能会使其特定于项目。
添加的内容之一是支持更复杂的类型,例如 Point
或 Size
。这些被序列化为不变的字符串,以便它们可以被解析回来。这是什么意思:
Console.WriteLine(myPoint.ToString())
结果,{X=64, Y=22}
不能直接转换回来,Point
缺少一个Parse/TryParse
方法。使用不变的字符串形式 64,22
可以将其转换回正确的类型。原链接代码简单使用:
Convert.ChangeType(setting.DefaultValue, t);
这将适用于简单类型,但不适用于 Point
、Font
等。我无法确定,但我认为这是使用 SettingsPropertyValue.Value
的一个简单错误而不是 .SerializedValue
.
3。代码
Public Class CustomSettingsProvider
Inherits SettingsProvider
' data we store for each item
Friend Class SettingsItem
Friend Name As String
'Friend SerializeAs As String ' not needed
Friend Value As String
Friend Roamer As Boolean
Friend Remove As Boolean ' mutable
'Friend VerString As String ' ToDo (?)
End Class
' used for node name
Private thisMachine As String
' loaded XML config
'Private xDoc As XDocument
Private UserConfigFilePath As String = ""
Private myCol As Dictionary(Of String, SettingsItem)
Public Sub New()
myCol = New Dictionary(Of String, SettingsItem)
Dim asm = Assembly.GetExecutingAssembly()
Dim verInfo = FileVersionInfo.GetVersionInfo(asm.Location)
Dim Company = verInfo.CompanyName
' product name may have no relation to file name...
Dim ProdName = verInfo.ProductName
' use this for assembly file name:
Dim modName = Path.GetFileNameWithoutExtension(asm.ManifestModule.Name)
' dont use FileVersionInfo;
' may want to omit the last element
'Dim ver = asm.GetName.Version
' uses `SpecialFolder.ApplicationData`
' since it will store Local and Roaming val;ues
UserConfigFilePath = Path.Combine(GetFolderPath(SpecialFolder.ApplicationData),
Company, modName,
"user.config")
' "CFG" prefix prevents illegal XML,
' the FOO suffix is to emulate a different machine
thisMachine = "CFG" & My.Computer.Name & "_FOO"
End Sub
' boilerplate
Public Overrides Property ApplicationName As String
Get
Return Assembly.GetExecutingAssembly().ManifestModule.Name
End Get
Set(value As String)
End Set
End Property
' boilerplate
Public Overrides Sub Initialize(name As String, config As Specialized.NameValueCollection)
MyBase.Initialize(ApplicationName, config)
End Sub
' conversion helper in place of a 'Select Case GetType(foo)'
Private Shared Conversion As Func(Of Object, Object)
Public Overrides Function GetPropertyValues(context As SettingsContext,
collection As SettingsPropertyCollection) As SettingsPropertyValueCollection
' basically, create a Dictionary entry for each setting,
' store the converted value to it
' Add an entry when something is added
'
' This is called the first time you get a setting value
If myCol.Count = 0 Then
LoadData()
End If
Dim theSettings = New SettingsPropertyValueCollection()
Dim tValue As String = ""
' SettingsPropertyCollection is like a Shopping list
' of props that VS/VB wants the value for
For Each setItem As SettingsProperty In collection
Dim value As New SettingsPropertyValue(setItem)
value.IsDirty = False
If myCol.ContainsKey(setItem.Name) Then
value.SerializedValue = myCol(setItem.Name)
tValue = myCol(setItem.Name).Value
Else
value.SerializedValue = setItem.DefaultValue
tValue = setItem.DefaultValue.ToString
End If
' ToDo: Enums will need an extra step
Conversion = Function(v) TypeDescriptor.
GetConverter(setItem.PropertyType).
ConvertFromInvariantString(v.ToString())
value.PropertyValue = Conversion(tValue)
theSettings.Add(value)
Next
Return theSettings
End Function
Public Overrides Sub SetPropertyValues(context As SettingsContext,
collection As SettingsPropertyValueCollection)
' this is not called when you set a new value
' rather, NET has one or more changed values that
' need to be saved, so be sure to save them to disk
Dim names As List(Of String) = myCol.Keys.ToList
Dim sItem As SettingsItem
For Each item As SettingsPropertyValue In collection
sItem = New SettingsItem() With {
.Name = item.Name,
.Value = item.SerializedValue.ToString(),
.Roamer = IsRoamer(item.Property)
}
'.SerializeAs = item.Property.SerializeAs.ToString(),
names.Remove(item.Name)
If myCol.ContainsKey(sItem.Name) Then
myCol(sItem.Name) = sItem
Else
myCol.Add(sItem.Name, sItem)
End If
Next
' flag any no longer used
' do not use when specifying a provider per-setting!
For Each s As String In names
myCol(s).Remove = True
Next
SaveData()
End Sub
' detect if a setting is tagged as Roaming
Private Function IsRoamer(prop As SettingsProperty) As Boolean
Dim r = prop.Attributes.
Cast(Of DictionaryEntry).
FirstOrDefault(Function(q) TypeOf q.Value Is SettingsManageabilityAttribute)
Return r.Key IsNot Nothing
End Function
Private Sub LoadData()
' load from disk
If File.Exists(UserConfigFilePath) = False Then
CreateNewConfig()
End If
Dim xDoc = XDocument.Load(UserConfigFilePath)
Dim items As IEnumerable(Of XElement)
Dim item As SettingsItem
items = xDoc.Element(CONFIG).
Element(COMMON).
Elements(SETTING)
' load the common settings
For Each xitem As XElement In items
item = New SettingsItem With {.Name = xitem.Attribute(ITEMNAME).Value,
.Roamer = False}
'.SerializeAs = xitem.Attribute(SERIALIZE_AS).Value,
item.Value = xitem.Value
myCol.Add(item.Name, item)
Next
' First check if there is a machine node
If xDoc.Element(CONFIG).Element(thisMachine) Is Nothing Then
' nope, add one
xDoc.Element(CONFIG).Add(New XElement(thisMachine))
End If
items = xDoc.Element(CONFIG).
Element(thisMachine).
Elements(SETTING)
For Each xitem As XElement In items
item = New SettingsItem With {.Name = xitem.Attribute(ITEMNAME).Value,
.Roamer = True}
'.SerializeAs = xitem.Attribute(SERIALIZE_AS).Value,
item.Value = xitem.Value
myCol.Add(item.Name, item)
Next
' we may have changed the XDOC, by adding a machine node
' save the file
xDoc.Save(UserConfigFilePath)
End Sub
Private Sub SaveData()
' write to disk
Dim xDoc = XDocument.Load(UserConfigFilePath)
Dim roamers = xDoc.Element(CONFIG).
Element(thisMachine)
Dim locals = xDoc.Element(CONFIG).
Element(COMMON)
Dim item As XElement
Dim section As XElement
For Each kvp As KeyValuePair(Of String, SettingsItem) In myCol
If kvp.Value.Roamer Then
section = roamers
Else
section = locals
End If
item = section.Elements().
FirstOrDefault(Function(q) q.Attribute(ITEMNAME).Value = kvp.Key)
If item Is Nothing Then
' found a new item
Dim newItem = New XElement(SETTING)
newItem.Add(New XAttribute(ITEMNAME, kvp.Value.Name))
'newItem.Add(New XAttribute(SERIALIZE_AS, kvp.Value.SerializeAs))
newItem.Value = If(String.IsNullOrEmpty(kvp.Value.Value), "", kvp.Value.Value)
section.Add(newItem)
Else
If kvp.Value.Remove Then
item.Remove()
Else
item.Value = If(String.IsNullOrEmpty(kvp.Value.Value), "", kvp.Value.Value)
End If
End If
Next
xDoc.Save(UserConfigFilePath)
End Sub
' used in the XML
Const CONFIG As String = "configuration"
Const SETTING As String = "setting"
Const COMMON As String = "CommonShared"
Const ITEMNAME As String = "name"
'Const SERIALIZE_AS As String = "serializeAs"
'
Private Sub CreateNewConfig()
Dim fpath = Path.GetDirectoryName(UserConfigFilePath)
Directory.CreateDirectory(fpath)
Dim xDoc = New XDocument
xDoc.Declaration = New XDeclaration("1.0", "utf-8", "true")
Dim cfg = New XElement(CONFIG)
cfg.Add(New XElement(COMMON))
cfg.Add(New XElement(thisMachine))
xDoc.Add(cfg)
xDoc.Save(UserConfigFilePath)
End Sub
End Class
仅仅为了从路径中删除证据哈希就需要大量代码,但这是 MS 推荐的。这也可能是唯一的方法:获取文件的 ConfigurationManager
中的 属性 是只读的,并且由代码支持。
结果:
实际的 XML 与前面显示的 local/common 和机器特定部分一样。我使用了几个不同的应用程序名称并进行了各种测试:
忽略版本部分。如前所述,已被删除。否则文件夹是正确的 - 如上所述,当涉及到 AppName 段时,您有一些选择。
重要提示
- 除非相关应用程序访问设置 属性
,否则不会调用提供程序中的加载方法
- 加载后,应用程序结束时(使用 VB 框架)将调用 Save 方法,无论代码是否更改任何内容
- NET 似乎只保存与默认值不同的设置。使用自定义提供程序时,所有值都标记为
IsDirty
为真,UsingDefaultValue
为假。
- If/when 已加载,返回所有值,NET 仅在应用程序的整个生命周期中从该集合中获取值
我主要关心的是类型的正确转换和 local/roaming 支持。我没有检查每一个可能的类型。特别是自定义类型和枚举(我知道枚举需要额外处理)。
值得注意的是,使用 DataTable
可以使 更简单 。 你不需要 SettingsItem
class,集合,没有 XDoc(使用 .WriteXML
/ .ReadXml
)。创建和组织 XElements 的所有代码也消失了。
生成的 XML 文件不同,但那只是函数的形式。总共可以去掉60行左右的代码,而且更简单
资源
- SettingsProvider Class
- MSDN blog entry about Settings
- Whosebug:Custom path of the user.config“查克”使用更好命名的暂存变量
- 计算器:MachineName problems in XML
- 完成后我才看到这篇 CodeProject 文章:Creating a Custom Settings Provider 这似乎是一种在每个设置基础上使用自定义提供程序的不同方法。
目前我的应用程序的用户设置存储在这个默认目录中:
C:\Users\{User Name}\AppData\Roaming\{Company Name}\{Assembly Name}.vshos_Url_{Hash}\{Assembly Version}
我知道默认的 Microsoft 命名规则的含义,我的问题是:如何在执行时或通过修改 appconfig 文件来更改默认文件夹?
我的意图是只能处理将保存我的应用程序的用户设置的目录,例如我想将用户设置文件保存在这个目录中:
C:\Users\{User Name}\AppData\Roaming\{Assembly Name}
我知道这是可以实现的,因为我见过很多 .NET 应用程序可以将其 userconfig 文件存储在自定义漫游文件夹中,该文件夹不遵循 Microsoft 默认规则以及未处理的哈希和其他烦人的规则命名规则。
我见过很多与此相关的问题,例如:
要做任何特别的事情,使用带有您自己的配置文件的 XDoc 或 LinqXML 会容易得多。
这样您就可以将它们保存在任何您喜欢的地方,并且不会遇到其他问题,例如:
存在该命名约定,以便 NET 可以确保加载了正确的设置。由于您已经将管理设置的控制权交给了 NET Framework/VB 应用程序框架,因此它还承担了确保应用加载正确设置集的责任。在这种情况下,证据散列用于唯一地识别一个 WindowsApplication1
与另一个(除其他外)。
I know this is possible to acchieve, because I've seen much .NET applications that can store its userconfig file in a custom Roaming folder
这是可能的,但我不确定一切是否与您得出的结论完全一致。我非常怀疑许多应用程序在使用自定义设置 class.
可以更轻松地将 XML 文件保存到该位置时会经历麻烦来实现自定义提供程序简单的解决方案
自己写用户选项class,自己序列化。例如,Shared/static 方法可用于在非常少的代码中反序列化 class(这恰好使用 JSON):
Friend Shared Function Load() As UserOptions
' create instance for default on new install
Dim u As New UserOptions
If File.Exists(filePath) Then
' filepath can be anywhere you have access to!
Dim jstr = File.ReadAllText(filePath)
If String.IsNullOrEmpty(jstr) = False Then
u = JsonConvert.DeserializeObject(Of UserOptions)(jstr)
End If
End If
Return u
End Function
实现它的应用程序:
UOpt = UserOptions.Load()
在专家中,您可以完全控制文件的保存位置,并且可以使用您喜欢的任何序列化程序。最重要的是,它 简单 - 代码比下面显示的要少得多。
缺点 是使用它的代码必须手动加载和保存它们(在应用程序事件中很容易处理),并且没有花哨的设计器。
漫长而曲折的道路:自定义设置提供程序
自定义 SettingsProvider
允许您更改设置的处理、保存和加载方式,包括更改文件夹位置。
这个问题的重点是更改文件位置。问题是您的应用没有一种(干净、简单)的方式与您的 SettingsProvider
通信以指定文件夹。提供者需要能够在内部解决这个问题,当然要保持一致。
大多数人想要做的不仅仅是更改使用的文件夹名称。例如,在玩耍时,我使用了一个 SQLite 数据库来代替 XML,它反映了代码使用的结构。这使得加载本地和正确的漫游值变得非常容易。如果一直采用这种方法,代码可以大大简化,很可能整个升级过程。因此,该提供商考虑了其中一些更广泛的需求。
即使您只想更改文件名,也有 2 个关键注意事项:
本地与漫游
将提供程序编码为始终存储在 AppData\Roaming
但写入不合格的本地设置是不负责任的。区分它们是一种不应为了省略文件夹名称中的证据哈希而牺牲的功能。
注意:每个 Setting
都可以设置为 Roaming
或 Local
值:在设置编辑器中选择一个设置,打开属性窗格 - 更改 Roaming
为真。
似乎在处理自定义 SettingsProvider
将本地和漫游保存到同一个文件但在不同部分中的(非常)几个问题上达成了共识。这很有意义 - 并且比从 2 个文件加载更简单 - 所以使用的 XML 结构是:
<configuration>
<CommonShared>
<setting name="FirstRun">True</setting>
<setting name="StartTime">15:32:18</setting>
...
</CommonShared>
<MACHINENAME_A>
<setting name="MainWdwLocation">98, 480</setting>
<setting name="myGuid">d62eb904-0bb9-4897-bb86-688d974db4a6</setting>
<setting name="LastSaveFolder">C:\Folder ABC</setting>
</MACHINENAME_A>
<MACHINENAME_B>
<setting name="MainWdwLocation">187, 360</setting>
<setting name="myGuid">a1f8d5a5-f7ec-4bf9-b7b8-712e80c69d93</setting>
<setting name="LastSaveFolder">C:\Folder XYZ</setting>
</MACHINENAME_B>
</configuration>
漫游项目存储在以使用它们的机器名称命名的部分中。保留 <NameSpace>.My.MySettings
节点可能有一些价值,但我不确定它的用途是什么。
我删除了 SerializeAs
元素,因为它未被使用。
版本
如果您调用 My.Settings.Upgrade
,则不会发生任何事情。尽管它是 Settings
方法,但它实际上是 ApplicationSettingsBase
中的内容,因此您的提供者不参与。
因此,如果自动递增最后一个元素,使用完整版本字符串作为文件夹的一部分会导致问题。琐碎的重建将创建一个新文件夹并丢失和孤立旧设置。当没有当前文件时,也许您可以查找并加载以前版本的值。然后也许删除那个旧的file/folder,所以总是只有一组可能的旧设置。随意添加大量的合并代码。
为了仅更改数据存储文件夹的主要目的,我删除了版本文件夹段。使用全局提供程序时,代码会自动累积设置。已删除的设置不会“泄漏”到应用程序中,因为 NET 不会为其请求值。唯一的问题是 XML.
中会有它的值我添加了代码来清除这些。如果您以后重复使用具有不同类型的设置名称,这将防止出现问题。例如,Foo
作为 Decimal
的旧保存值将无法与新的 Foo
作为 Size
一起使用。如果你彻底改变一种类型,事情仍然会很糟糕。不要那样做。
这个答案 Custom path of the user.config 为自定义提供程序提供了一个非常好的起点。它有一些问题并且遗漏了一些东西,但为任何提供商的一些典型步骤和样板代码提供了快速入门指南。由于许多人可能需要在此处进一步修改提供程序,因此可能值得一读(并投票)。
此处的代码借鉴了该答案的一些内容,并且:
- 添加各种改进
- 提供自定义路径
- 漫游设置检测
- 文件中的本地和漫游部分
- 正确处理复杂类型,例如
Point
或Size
- 检测并删除删除的设置
- 在VB
1。设置
在大多数情况下,您不能逐步 write/debug 这一点 - 在您完成之前几乎没有用。
- 添加对
System.Configuration
的引用
- 向您的项目添加一个新的 class
示例:
Imports System.Configuration
Public Class CustomSettingsProvider
Inherits SettingsProvider
End Class
接下来,转到设置设计器并添加一些设置以进行测试。将一些标记为漫游以进行完整测试。然后单击此处显示的 <> View Code
按钮:
显然有两种方法可以实现自定义提供程序。此处的代码将使用您的代码代替 My.MySettings
。您还可以通过在“属性”窗格中键入提供程序名称来基于每个设置指定自定义提供程序,并跳过此步骤的其余部分。我没有对此进行测试,但它应该是这样工作的。
为了使用“您”编写的新设置提供程序,它需要使用属性与 MySettings
相关联:
Imports System.Configuration
<SettingsProvider(GetType(ElectroZap.CustomSettingsProvider))>
Partial Friend NotInheritable Class MySettings
End Class
'ElektroZap' 是您的根命名空间,'ElektroApp' 是您的应用程序名称。构造函数中的代码可以更改为使用产品名称或模块名称。
我们完成了那个文件;保存并关闭它。
2。设置提供者
首先,请注意此 CustomProvider 是通用的,只需将其指定为 SettingsProvider
即可与任何应用程序一起使用。但它实际上只做了两件事:
- 使用自定义路径
- 将本地和漫游设置合并到一个文件中
通常情况下,在求助于自定义提供程序之前会有更长的待办事项列表,因此对于许多人来说,这可能只是提供其他事情的起点。请记住,某些更改可能会使其特定于项目。
添加的内容之一是支持更复杂的类型,例如 Point
或 Size
。这些被序列化为不变的字符串,以便它们可以被解析回来。这是什么意思:
Console.WriteLine(myPoint.ToString())
结果,{X=64, Y=22}
不能直接转换回来,Point
缺少一个Parse/TryParse
方法。使用不变的字符串形式 64,22
可以将其转换回正确的类型。原链接代码简单使用:
Convert.ChangeType(setting.DefaultValue, t);
这将适用于简单类型,但不适用于 Point
、Font
等。我无法确定,但我认为这是使用 SettingsPropertyValue.Value
的一个简单错误而不是 .SerializedValue
.
3。代码
Public Class CustomSettingsProvider
Inherits SettingsProvider
' data we store for each item
Friend Class SettingsItem
Friend Name As String
'Friend SerializeAs As String ' not needed
Friend Value As String
Friend Roamer As Boolean
Friend Remove As Boolean ' mutable
'Friend VerString As String ' ToDo (?)
End Class
' used for node name
Private thisMachine As String
' loaded XML config
'Private xDoc As XDocument
Private UserConfigFilePath As String = ""
Private myCol As Dictionary(Of String, SettingsItem)
Public Sub New()
myCol = New Dictionary(Of String, SettingsItem)
Dim asm = Assembly.GetExecutingAssembly()
Dim verInfo = FileVersionInfo.GetVersionInfo(asm.Location)
Dim Company = verInfo.CompanyName
' product name may have no relation to file name...
Dim ProdName = verInfo.ProductName
' use this for assembly file name:
Dim modName = Path.GetFileNameWithoutExtension(asm.ManifestModule.Name)
' dont use FileVersionInfo;
' may want to omit the last element
'Dim ver = asm.GetName.Version
' uses `SpecialFolder.ApplicationData`
' since it will store Local and Roaming val;ues
UserConfigFilePath = Path.Combine(GetFolderPath(SpecialFolder.ApplicationData),
Company, modName,
"user.config")
' "CFG" prefix prevents illegal XML,
' the FOO suffix is to emulate a different machine
thisMachine = "CFG" & My.Computer.Name & "_FOO"
End Sub
' boilerplate
Public Overrides Property ApplicationName As String
Get
Return Assembly.GetExecutingAssembly().ManifestModule.Name
End Get
Set(value As String)
End Set
End Property
' boilerplate
Public Overrides Sub Initialize(name As String, config As Specialized.NameValueCollection)
MyBase.Initialize(ApplicationName, config)
End Sub
' conversion helper in place of a 'Select Case GetType(foo)'
Private Shared Conversion As Func(Of Object, Object)
Public Overrides Function GetPropertyValues(context As SettingsContext,
collection As SettingsPropertyCollection) As SettingsPropertyValueCollection
' basically, create a Dictionary entry for each setting,
' store the converted value to it
' Add an entry when something is added
'
' This is called the first time you get a setting value
If myCol.Count = 0 Then
LoadData()
End If
Dim theSettings = New SettingsPropertyValueCollection()
Dim tValue As String = ""
' SettingsPropertyCollection is like a Shopping list
' of props that VS/VB wants the value for
For Each setItem As SettingsProperty In collection
Dim value As New SettingsPropertyValue(setItem)
value.IsDirty = False
If myCol.ContainsKey(setItem.Name) Then
value.SerializedValue = myCol(setItem.Name)
tValue = myCol(setItem.Name).Value
Else
value.SerializedValue = setItem.DefaultValue
tValue = setItem.DefaultValue.ToString
End If
' ToDo: Enums will need an extra step
Conversion = Function(v) TypeDescriptor.
GetConverter(setItem.PropertyType).
ConvertFromInvariantString(v.ToString())
value.PropertyValue = Conversion(tValue)
theSettings.Add(value)
Next
Return theSettings
End Function
Public Overrides Sub SetPropertyValues(context As SettingsContext,
collection As SettingsPropertyValueCollection)
' this is not called when you set a new value
' rather, NET has one or more changed values that
' need to be saved, so be sure to save them to disk
Dim names As List(Of String) = myCol.Keys.ToList
Dim sItem As SettingsItem
For Each item As SettingsPropertyValue In collection
sItem = New SettingsItem() With {
.Name = item.Name,
.Value = item.SerializedValue.ToString(),
.Roamer = IsRoamer(item.Property)
}
'.SerializeAs = item.Property.SerializeAs.ToString(),
names.Remove(item.Name)
If myCol.ContainsKey(sItem.Name) Then
myCol(sItem.Name) = sItem
Else
myCol.Add(sItem.Name, sItem)
End If
Next
' flag any no longer used
' do not use when specifying a provider per-setting!
For Each s As String In names
myCol(s).Remove = True
Next
SaveData()
End Sub
' detect if a setting is tagged as Roaming
Private Function IsRoamer(prop As SettingsProperty) As Boolean
Dim r = prop.Attributes.
Cast(Of DictionaryEntry).
FirstOrDefault(Function(q) TypeOf q.Value Is SettingsManageabilityAttribute)
Return r.Key IsNot Nothing
End Function
Private Sub LoadData()
' load from disk
If File.Exists(UserConfigFilePath) = False Then
CreateNewConfig()
End If
Dim xDoc = XDocument.Load(UserConfigFilePath)
Dim items As IEnumerable(Of XElement)
Dim item As SettingsItem
items = xDoc.Element(CONFIG).
Element(COMMON).
Elements(SETTING)
' load the common settings
For Each xitem As XElement In items
item = New SettingsItem With {.Name = xitem.Attribute(ITEMNAME).Value,
.Roamer = False}
'.SerializeAs = xitem.Attribute(SERIALIZE_AS).Value,
item.Value = xitem.Value
myCol.Add(item.Name, item)
Next
' First check if there is a machine node
If xDoc.Element(CONFIG).Element(thisMachine) Is Nothing Then
' nope, add one
xDoc.Element(CONFIG).Add(New XElement(thisMachine))
End If
items = xDoc.Element(CONFIG).
Element(thisMachine).
Elements(SETTING)
For Each xitem As XElement In items
item = New SettingsItem With {.Name = xitem.Attribute(ITEMNAME).Value,
.Roamer = True}
'.SerializeAs = xitem.Attribute(SERIALIZE_AS).Value,
item.Value = xitem.Value
myCol.Add(item.Name, item)
Next
' we may have changed the XDOC, by adding a machine node
' save the file
xDoc.Save(UserConfigFilePath)
End Sub
Private Sub SaveData()
' write to disk
Dim xDoc = XDocument.Load(UserConfigFilePath)
Dim roamers = xDoc.Element(CONFIG).
Element(thisMachine)
Dim locals = xDoc.Element(CONFIG).
Element(COMMON)
Dim item As XElement
Dim section As XElement
For Each kvp As KeyValuePair(Of String, SettingsItem) In myCol
If kvp.Value.Roamer Then
section = roamers
Else
section = locals
End If
item = section.Elements().
FirstOrDefault(Function(q) q.Attribute(ITEMNAME).Value = kvp.Key)
If item Is Nothing Then
' found a new item
Dim newItem = New XElement(SETTING)
newItem.Add(New XAttribute(ITEMNAME, kvp.Value.Name))
'newItem.Add(New XAttribute(SERIALIZE_AS, kvp.Value.SerializeAs))
newItem.Value = If(String.IsNullOrEmpty(kvp.Value.Value), "", kvp.Value.Value)
section.Add(newItem)
Else
If kvp.Value.Remove Then
item.Remove()
Else
item.Value = If(String.IsNullOrEmpty(kvp.Value.Value), "", kvp.Value.Value)
End If
End If
Next
xDoc.Save(UserConfigFilePath)
End Sub
' used in the XML
Const CONFIG As String = "configuration"
Const SETTING As String = "setting"
Const COMMON As String = "CommonShared"
Const ITEMNAME As String = "name"
'Const SERIALIZE_AS As String = "serializeAs"
'
Private Sub CreateNewConfig()
Dim fpath = Path.GetDirectoryName(UserConfigFilePath)
Directory.CreateDirectory(fpath)
Dim xDoc = New XDocument
xDoc.Declaration = New XDeclaration("1.0", "utf-8", "true")
Dim cfg = New XElement(CONFIG)
cfg.Add(New XElement(COMMON))
cfg.Add(New XElement(thisMachine))
xDoc.Add(cfg)
xDoc.Save(UserConfigFilePath)
End Sub
End Class
仅仅为了从路径中删除证据哈希就需要大量代码,但这是 MS 推荐的。这也可能是唯一的方法:获取文件的 ConfigurationManager
中的 属性 是只读的,并且由代码支持。
结果:
实际的 XML 与前面显示的 local/common 和机器特定部分一样。我使用了几个不同的应用程序名称并进行了各种测试:
忽略版本部分。如前所述,已被删除。否则文件夹是正确的 - 如上所述,当涉及到 AppName 段时,您有一些选择。
重要提示
- 除非相关应用程序访问设置 属性 ,否则不会调用提供程序中的加载方法
- 加载后,应用程序结束时(使用 VB 框架)将调用 Save 方法,无论代码是否更改任何内容
- NET 似乎只保存与默认值不同的设置。使用自定义提供程序时,所有值都标记为
IsDirty
为真,UsingDefaultValue
为假。 - If/when 已加载,返回所有值,NET 仅在应用程序的整个生命周期中从该集合中获取值
我主要关心的是类型的正确转换和 local/roaming 支持。我没有检查每一个可能的类型。特别是自定义类型和枚举(我知道枚举需要额外处理)。
值得注意的是,使用 DataTable
可以使 更简单 。 你不需要 SettingsItem
class,集合,没有 XDoc(使用 .WriteXML
/ .ReadXml
)。创建和组织 XElements 的所有代码也消失了。
生成的 XML 文件不同,但那只是函数的形式。总共可以去掉60行左右的代码,而且更简单
资源
- SettingsProvider Class
- MSDN blog entry about Settings
- Whosebug:Custom path of the user.config“查克”使用更好命名的暂存变量
- 计算器:MachineName problems in XML
- 完成后我才看到这篇 CodeProject 文章:Creating a Custom Settings Provider 这似乎是一种在每个设置基础上使用自定义提供程序的不同方法。