簡體   English   中英

如何更改.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命名規則的含義,我的問題是:如何在執行時更改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路徑為定制提供程序提供了一個很好的起點。 它有一些問題,缺少一些東西,但是提供了任何提供程序特有的一些步驟和樣板代碼的快速入門指南。 由於許多人可能需要在此處進一步修改提供程序,因此可能值得閱讀(並贊成)。

這里的代碼從該答案中借用了一些東西,並且:

  • 添加各種改進
  • 提供自定義路徑
  • 檢測設置為漫游的設置
  • 文件中的“本地和漫游”部分
  • 正確處理PointSize等復雜類型
  • 檢測並修剪刪除的設置
  • 在VB中

1.設定

在大多數情況下,您無法以增量方式編寫/調試此文件-在完成之前幾乎沒有用。

  • 添加對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”是您的應用程序名稱。 可以將構造函數中的代碼更改為使用產品名稱或模塊名稱。

我們已經完成了該文件。 保存並關閉它。

2. SettingsProvider

首先,請注意,此CustomProvider是通用的,只需將其指定為SettingsProvider與任何應用一起使用。 但這實際上只做兩件事:

  • 使用自定義路徑
  • 將本地和漫游設置合並到一個文件中

通常,在求助於自定義提供程序之前,一個待辦事項列表會更長,因此對於許多人來說,這可能只是“其他事物”的起點。 請記住,某些更改可能使其特定於項目。


添加的功能之一是支持更復雜的類型,例如PointSize 這些被序列化為不變字符串,以便可以解析它們。 這意味着什么:

Console.WriteLine(myPoint.ToString())

結果{X=64, Y=22}無法直接轉換回去,並且Point缺少Parse/TryParse方法。 使用不變字符串形式64,22可以將其轉換回正確的類型。 原始的鏈接代碼簡單地使用了:

Convert.ChangeType(setting.DefaultValue, t);

這將適用於簡單的類型,但不適用於PointFont等。我無法確定,但是我認為這是使用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"

    ' 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段中,您可以使用一些選項。

重要筆記

  • 除非且直到相關應用程序訪問Settings屬性,否則不會調用提供程序中的Load方法。
  • 加載后,無論代碼是否更改,應用程序結束時(使用VB Framework)將調用Save方法。
  • NET似乎只保存與默認值不同的設置。 使用自定義提供程序時,所有值都標記為IsDirty為true和UsingDefaultValue為false。
  • 如果/在加載時,將返回所有值,並且NET會在應用程序的整個生命周期內從該集合中獲取值

我主要關心的是類型和本地/漫游支持的正確轉換。 沒有檢查每個可能的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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM