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 Application Framework に委ねたので、アプリが適切な設定セットを読み込んでいることを確認する責任も負っています。その場合、証拠ハッシュを使用して、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()

長所の中でも、ファイルの保存場所を完全に制御でき、好きなシリアライザーを使用できます。何よりも、それは単純です。以下に示すよりもはるかに少ないコードです。

短所は、それを使用するコードがそれらを手動でロードおよび保存する必要があり (アプリケーション イベントで簡単に処理される)、そのための洗練されたデザイナーがないことです。

長く曲がりくねった道: カスタム設定プロバイダー

カスタムSettingsProviderを使用すると、フォルダーの場所の変更など、設定の処理、保存、および読み込み方法を変更できます。

この質問は、ファイルの場所の変更に焦点を絞っています。SettingsProvider問題は、フォルダーを指定するためにアプリがユーザーと対話する (クリーンで簡単な) 方法がないことです。プロバイダーは、それを内部で解決できる必要があり、もちろん一貫性が保たれている必要があります。

ほとんどの人は、使用するフォルダー名を変更するだけではありません。たとえば、いろいろ試してみたところ、XML の代わりに、コードが使用する構造を反映した SQLite データベースを使用しました。これにより、ローカルおよび正しいローミング値のロードが非常に簡単になりました。このアプローチが完全に採用された場合、コードは大幅に簡素化され、おそらくアップグレード プロセス全体が簡素化される可能性があります。したがって、このプロバイダーは、これらの幅広いニーズのいくつかを考慮に入れています。

ファイル名を変更したい場合でも、重要な考慮事項が 2 つあります。

ローカル vs ローミング

常に保存するようにプロバイダーをコーディングするAppData\Roamingが、そこに修飾されていないローカル設定を書き込むことは無責任です。それらを区別することは、フォルダー名の証拠ハッシュを除外するために犠牲にするべきではない機能です。

注: それぞれをまたは値Settingとして設定できます。設定エディターで設定を選択して、[プロパティ] ペインを開き、[ True] に変更します。RoamingLocalRoaming

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あるため、プロバイダーは関与しません。

その結果、フォルダーの一部として完全なバージョン文字列を使用すると、最後の要素を自動インクリメントする場合に問題が発生します。些細な再構築により、新しいフォルダーが作成され、古い設定が失われて孤立します。おそらく、現在のファイルがないときに、以前のバージョンの値を探してロードできます。次に、おそらくその古いファイル/フォルダーを削除して、可能な古い設定のセットが常に 1 つだけになるようにします。大量のマージ コードを自由に追加してください。

データ ストア フォルダーを変更することを主な目的として、バージョン フォルダー セグメントを削除しました。グローバル プロバイダーを使用する場合、コードは自動的に設定を蓄積します。削除された設定は、NET がその値を要求しないため、アプリに「漏れる」ことはありません。唯一の問題は、XML に値が存在することです。

これらをパージするコードを追加しました。これにより、後で別のタイプの設定名を再利用した場合の問題を防ぐことができます。Fooasの古い保存値は、たとえばDecimal新しいFooasでは機能しません。Sizeタイプを根本的に変更すると、事態は依然として悪化します。そうしないでください。


この回答user.config のカスタム パスは、カスタム プロバイダーの非常に優れた出発点を提供します。いくつかの問題があり、欠けているものもいくつかありますが、いくつかの手順のクイック スタート ガイドと、任意のプロバイダーに典型的なボイラープレート コードを提供します。多くの人がここでプロバイダをさらに変更する必要があるかもしれないので、読む価値があるかもしれません (そして賛成票を投じます)。

ここのコードは、その回答からいくつかのものを借りています。

  • さまざまな改良を加える
  • カスタムパスを提供します
  • ローミングに設定された設定の検出
  • ファイルのローカルおよびローミング セクション
  • Pointまたはなどの複雑な型の適切な処理Size
  • 削除された設定を検出して削除する
  • VBにあります

1.セットアップ

ほとんどの場合、これをインクリメンタルに書き込んだりデバッグしたりすることはできません。完了するまではほとんど機能しません。

  • への参照を追加System.Configuration
  • プロジェクトに新しいクラスを追加する

例:

Imports System.Configuration 
Public Class CustomSettingsProvider
    Inherits SettingsProvider
End Class

次に、設定デザイナーに移動し、テスト用の設定をいくつか追加します。完全なテストのために、いくつかを Roaming としてタグ付けします。次に、次の<> View Codeボタンをクリックします。

ここに画像の説明を入力 みんな大好きフリーハンドサークル!

カスタム プロバイダーを実装するには、明らかに 2 つの方法があります。ここのコードでは、代わりにあなたのものを使用しますMy.MySettings。[プロパティ] ペインにプロバイダー名を入力して、設定ごとにカスタム プロバイダーを指定し、この手順の残りをスキップすることもできます。私はこれをテストしませんでしたが、それがどのように機能するかです。

「あなた」が作成した新しい設定プロバイダーをMySettings使用するには、属性を使用して関連付ける必要があります。

Imports System.Configuration 

<SettingsProvider(GetType(ElectroZap.CustomSettingsProvider))>
Partial Friend NotInheritable Class MySettings
End Class

ちなみに、「ElektroZap」はルート NameSpace で、「ElektroApp」はアプリ名です。コンストラクターのコードは、製品名またはモジュール名を使用するように変更できます。

そのファイルはこれで終わりです。保存して閉じます。

2.設定プロバイダー

まず、この CustomProvider は汎用的であり、SettingsProvider. しかし、実際には次の 2 つのことしか行いません。

  • カスタムパスを使用
  • ローカル設定とローミング設定を 1 つのファイルにマージします

通常、カスタム プロバイダーに頼る前に ToDo リストが長くなるため、多くの場合、これは他のことの開始点を提供するだけかもしれません。変更によっては、プロジェクト固有のものになる可能性があることに注意してください。


Point追加されたものの 1 つは、またはなどのより複雑な型のサポートですSize。これらは不変文字列としてシリアル化されるため、解析して戻すことができます。これが意味することは次のとおりです。

Console.WriteLine(myPoint.ToString())

結果は{X=64, Y=22}直接変換できずPointParse/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"

    ' 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 フレームワークを使用して) アプリの終了時に Save メソッドが呼び出されます。
  • NET は、既定値とは異なる設定のみを保存するようです。カスタム プロバイダーを使用すると、すべての値がIsDirtytrue およびUsingDefaultValuefalse としてマークされます。
  • ロードされた場合、すべての値が返され、NET はアプリの存続期間を通じてそのコレクションから値を取得するだけです。

私の主な関心事は、型の正しい変換とローカル/ローミングのサポートでした。考えられるすべての Type をチェックしたわけではありません。特に、カスタム型と列挙型 (列挙型には追加の処理が必要になることはわかっています)。


DataTablea を使用すると、これがはるかに簡単になることに注意してください。SettingsItemクラス、コレクション、XDoc は必要ありません( .WriteXML/を使用.ReadXml)。XElements を作成および整理するためのコードもすべてなくなります。

結果として得られる XML ファイルは異なりますが、それはフォーム フォローイング関数にすぎません。全部で約 60 行のコードを削除することができ、単純化されています。

資力

于 2016-07-23T00:58:40.617 に答える