2

目前,我的应用程序的用户设置存储在此默认目录中:

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 应用程序可以将其用户配置文件存储在自定义漫游文件夹中,该文件夹不遵循带有未处理哈希和其他烦人命名规则的 Microsoft 默认规则。

4

2 回答 2

8

存在该命名约定以便 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

这是可能的,但我不确定一切都与您得出的结论完全一致。我非常怀疑,当许多应用程序可以使用自定义设置类更轻松地将 XML 文件保存到该位置时,是否会遇到麻烦来实现自定义提供程序。

简单的解决方案

编写您自己的用户选项类,并自己序列化它。例如,可以使用共享/静态方法以非常少的代码反序列化类(这恰好使用 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()

Pros中,您可以完全控制文件的保存位置,并且可以使用您喜欢的任何序列化程序。最重要的是,它很简单——比下面介绍的代码少得多。

缺点是使用它的代码必须手动加载和保存它们(在应用程序事件中很容易处理),并且没有花哨的设计器。

漫长而曲折的道路:自定义设置提供程序

自定义SettingsProvider将允许您更改设置的处理、保存和加载方式,包括更改文件夹位置。

这个问题只关注更改文件位置。问题是您的应用程序没有一种(干净,简单)的方式与您交谈SettingsProvider以指定文件夹。提供者需要能够在内部解决这个问题,当然要保持一致。

大多数人会想做的不仅仅是更改使用的文件夹名称。例如,在玩耍时,我使用了一个 SQLite 数据库来代替 XML,它反映了代码使用的结构。这使得加载本地和正确的漫游值变得非常容易。如果一直采用这种方法,代码可以大大简化,很可能整个升级过程。因此,该提供商考虑了其中一些更广泛的需求。

即使您只想更改文件名,也有 2 个关键注意事项:

本地与漫游

将提供程序编码为始终存储AppData\Roaming但写入不合格的本地设置是不负责任的。区分它们是一种不应该为了省略文件夹名称中的证据散列而牺牲的能力。

注意:每个Setting都可以设置为RoamingLocal值:在设置编辑器中选择设置后,打开属性窗格 - 更改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方法,但它实际上是 in 中的东西ApplicationSettingsBase,因此您的提供者不参与其中。

因此,如果您自动递增最后一个元素,则使用完整版本字符串作为文件夹的一部分会导致问题。简单的重建将创建一个新文件夹并丢失和孤立旧设置。当没有当前文件时,也许您可​​以查找并加载先前版本的值。然后也许删除那个旧文件/文件夹,所以总是只有一组可能的旧设置。随意添加大量合并代码。

出于仅更改数据存储文件夹的主要目的,我删除了版本文件夹段。使用全局提供程序时,代码会自动累积设置。已删除的设置不会“泄漏”到应用程序中,因为 NET 不会为其请求值。唯一的问题是它在 XML 中会有一个值。

我添加了代码来清除这些。如果您以后重用具有不同类型的设置名称,这将防止出现问题。Foo例如,旧的as保存值不适用于DecimalFoo的 as Size。如果你从根本上改变一种类型,事情仍然会变得很糟糕。不要那样做。


这个答案user.config 的自定义路径为自定义提供程序提供了一个非常好的起点。它有一些问题并且遗漏了一些东西,但为任何提供者的一些典型步骤和样板代码提供了快速入门指南。由于许多人可能需要在此处进一步修改提供程序,因此可能值得一读(和赞成)。

这里的代码从该答案中借用了一些东西,并且:

  • 添加各种改进
  • 提供自定义路径
  • 检测设置为漫游的设置
  • 文件中的本地和漫游部分
  • 正确处理复杂类型,例如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' 是你的根命名空间,'ElektroApp' 是你的应用程序名称,顺便说一下。可以将构造函数中的代码更改为使用产品名称或模块名称。

我们完成了那个文件;保存并关闭它。

2.设置提供者

首先,请注意,此 CustomProvider 是通用的,只需将其指定为SettingsProvider. 但它实际上只做两件事:

  • 使用自定义路径
  • 将本地和漫游设置合并到一个文件中

通常,在求助于自定义提供程序之前,会有一个更长的待办事项列表,所以对于许多人来说,这可能只是为其他事情提供了起点。请记住,某些更改可能使其特定于项目。


添加的其中一件事是支持更复杂的类型,例如Pointor Size。这些被序列化为不变的字符串,以便它们可以被解析回来。这意味着:

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 方法
  • 加载后,无论代码是否更改任何内容,都会在应用程序结束时调用 Save 方法(使用 VB 框架)
  • NET 似乎只保存与默认值不同的设置。使用自定义提供程序时,所有值都标记IsDirty为 true 和UsingDefaultValuefalse。
  • 如果/当加载时,所有值都被返回,NET 只是从该集合中获取值,直到应用程序的生命周期

我主要关心的是类型的正确转换和本地/漫游支持。我没有检查每一个可能的 Type。特别是自定义类型和枚举(我知道枚举需要额外处理)。


值得注意的是,使用 aDataTable使这变得更简单。 您不需要SettingsItem类、集合,也不需要 XDoc(使用.WriteXML/ .ReadXml)。所有用于创建和组织 XElements 的代码也都消失了。

生成的 XML 文件有所不同,但这只是形式跟随功能。总共可以删除大约 60 行代码,而且更简单。

资源

于 2016-07-23T00:58:40.617 回答
1

我已经看到了很多关于这个的问题,例如:https ://stackoverflow.com/a/15726277/495455

要做任何特别的事情,将 XDoc 或 LinqXML 与您自己的配置文件一起使用会更容易。

这样您就可以将它们保存在您喜欢的任何地方,并且不会遇到其他问题,例如:自定义配置部分只能在以管理员身份运行时保存/修改?

于 2016-07-21T10:54:31.307 回答