[英]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命名規則的含義,我的問題是:如何在執行時更改defaut文件夾或通過修改appconfig文件?
我的意圖是只能處理將應用程序的用戶設置保存到的目錄,例如,我想將用戶設置文件保存在以下目錄中:
C:\Users\{User Name}\AppData\Roaming\{Assembly Name}
我知道這是可以實現的,因為我已經看到很多.NET應用程序可以將其userconfig文件存儲在自定義漫游文件夾中,該文件夾不遵循Microsoft默認規則以及未處理的哈希和其他令人討厭的命名規則。
存在該命名約定,以便NET可以確保已加載正確的設置。 由於您已放棄了對NET Framework / VB應用程序框架的設置進行管理的控制,因此它還負責確保應用程序正在加載正確的設置集。 在這種情況下,證據散列用於將一個WindowsApplication1
與另一個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
有可能,但我不確定所有內容是否都符合您的結論。 我非常嚴重地懷疑,許多應用程序使用自定義設置類可以更輕松地將XML文件保存到該位置時,會麻煩地實現自定義提供程序。
編寫自己的用戶選項類,然后自己進行序列化。 例如,可以使用Shared / static方法以很少的代碼反序列化類(這恰好使用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()
在Pro中 ,您可以完全控制文件的保存位置,並且可以使用任何喜歡的序列化程序。 最重要的是,它很簡單 -比下面介紹的代碼少得多。
缺點是使用它的代碼必須手動加載和保存它們(在Application事件中很容易處理),並且沒有花哨的設計器。
自定義SettingsProvider
將允許您更改設置的處理,保存和加載方式,包括更改文件夾位置。
這個問題只集中在更改文件位置上。 問題在於,您的應用沒有一種(干凈,簡單)的方式與SettingsProvider
進行對話以指定文件夾。 提供者需要能夠在內部進行工作,當然必須保持一致。
除了更改所使用的文件夾名稱之外,大多數人還希望做更多的事情。 例如,在游戲中,我使用了一個SQLite數據庫來代替XML,該數據庫鏡像了代碼使用的結構。 這使得加載本地和正確的漫游值非常容易。 如果始終采用這種方法,則可以大大簡化代碼,甚至可以簡化整個升級過程。 因此,該提供商考慮了一些更廣泛的需求。
即使您只想更改文件名,也有兩個關鍵注意事項:
本地與漫游
編碼提供程序以始終存儲在AppData\\Roaming
但是編寫不合格的本地設置將是不負責任的。 區分它們是不應該為了消除文件夾名稱中的證據哈希而犧牲的功能。
注意:可以將每個Setting
設置為Roaming
值或Local
值:在“設置編輯器”中選擇一個設置,打開“屬性”窗格-將“ Roaming
更改為True。
在(自定義) 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>
漫游項存儲在使用它們的MachineName命名的節中。 保留<NameSpace>.My.MySettings
節點可能會有一些價值,但是我不確定它的作用是什么。
由於未使用SerializeAs
元素,因此刪除了它。
版本號
如果調用My.Settings.Upgrade
則不會發生任何事情。 盡管它是Settings
方法,但實際上它是ApplicationSettingsBase
,因此不涉及您的提供程序。
結果,如果您自動增加最后一個元素,則使用完整版本字符串作為文件夾的一部分會導致問題。 簡單的重建將創建一個新文件夾,並使舊設置丟失並孤立。 當沒有當前文件時,也許您可以查找並加載先前版本的值。 然后也許刪除該舊文件/文件夾,因此始終只有一組可能的舊設置。 隨意添加面條和合並代碼。
為了僅更改數據存儲文件夾的主要目的,我刪除了版本文件夾段。 使用全局提供程序時,代碼會自動累積設置。 已刪除的設置不會“泄漏”到應用程序中,因為NET不會要求它提供值。 唯一的問題是XML中將有一個值。
我添加了清除這些代碼。 如果您以后再使用其他類型的設置名稱,則可以防止出現問題。 例如, Foo
的舊保存值( Decimal
與新Foo
Size
使用。 如果您從根本上更改類型,事情仍然會很糟糕。 不要那樣做
這個答案user.config的Custom路徑為定制提供程序提供了一個很好的起點。 它有一些問題,缺少一些東西,但是提供了任何提供程序特有的一些步驟和樣板代碼的快速入門指南。 由於許多人可能需要在此處進一步修改提供程序,因此可能值得閱讀(並贊成)。
這里的代碼從該答案中借用了一些東西,並且:
Point
或Size
等復雜類型 在大多數情況下,您無法以增量方式編寫/調試此文件-在完成之前幾乎沒有用。
System.Configuration
的引用 例:
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”是您的根NameSpace,而“ ElektroApp”是您的應用程序名稱。 可以將構造函數中的代碼更改為使用產品名稱或模塊名稱。
我們已經完成了該文件。 保存並關閉它。
首先,請注意,此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
的簡單錯誤。
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"
' https://stackoverflow.com/a/11398536
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如前面顯示的本地/公共和計算機特定部分所示。 我使用了幾個不同的應用程序名稱,並測試了各種內容:
忽略版本部分。 如前所述,已被刪除。 否則,文件夾是正確的-如上所述,在AppName段中,您可以使用一些選項。
IsDirty
為true和UsingDefaultValue
為false。 我主要關心的是類型和本地/漫游支持的正確轉換。 我沒有檢查每個可能的Type 。 特別是自定義類型和枚舉(我知道枚舉將需要額外的處理)。
值得注意的是,使用DataTable
可以使此過程變得更加簡單。 您不需要SettingsItem
類,集合,不需要XDoc(使用.WriteXML
/ .ReadXml
)。 創建和組織XElement的所有代碼也都消失了。
生成的XML文件是不同的,但這僅是表單跟隨功能。 總共可以刪除大約60行代碼,這很簡單。
資源資源
我已經看到許多與此相關的問題,例如: https : //stackoverflow.com/a/15726277/495455
要執行任何特殊操作,將XDoc或LinqXML與您自己的配置文件一起使用會容易得多。
這樣,您可以將它們保存在任意位置,而不會遇到其他問題,例如: 自定義配置部分只能在以管理員身份運行時才能保存/修改?
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.