この命名規則は、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] に変更します。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>
ローミング項目は、それらが使用されている MachineName にちなんで名付けられたセクションに格納されます。<NameSpace>.My.MySettings
ノードを保持することには何らかの価値があるかもしれませんが、それがどのような目的に役立つかはわかりません。
SerializeAs
エレメントは使用しないので取り外しました。
バージョン
を呼び出しても何も起こりませんMy.Settings.Upgrade
。これはSettings
メソッドですが、実際には の何かでApplicationSettingsBase
あるため、プロバイダーは関与しません。
その結果、フォルダーの一部として完全なバージョン文字列を使用すると、最後の要素を自動インクリメントする場合に問題が発生します。些細な再構築により、新しいフォルダーが作成され、古い設定が失われて孤立します。おそらく、現在のファイルがないときに、以前のバージョンの値を探してロードできます。次に、おそらくその古いファイル/フォルダーを削除して、可能な古い設定のセットが常に 1 つだけになるようにします。大量のマージ コードを自由に追加してください。
データ ストア フォルダーを変更することを主な目的として、バージョン フォルダー セグメントを削除しました。グローバル プロバイダーを使用する場合、コードは自動的に設定を蓄積します。削除された設定は、NET がその値を要求しないため、アプリに「漏れる」ことはありません。唯一の問題は、XML に値が存在することです。
これらをパージするコードを追加しました。これにより、後で別のタイプの設定名を再利用した場合の問題を防ぐことができます。Foo
asの古い保存値は、たとえばDecimal
新しいFoo
asでは機能しません。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}
直接変換できず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"
' 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 は、既定値とは異なる設定のみを保存するようです。カスタム プロバイダーを使用すると、すべての値が
IsDirty
true およびUsingDefaultValue
false としてマークされます。
- ロードされた場合、すべての値が返され、NET はアプリの存続期間を通じてそのコレクションから値を取得するだけです。
私の主な関心事は、型の正しい変換とローカル/ローミングのサポートでした。考えられるすべての Type をチェックしたわけではありません。特に、カスタム型と列挙型 (列挙型には追加の処理が必要になることはわかっています)。
DataTable
a を使用すると、これがはるかに簡単になることに注意してください。SettingsItem
クラス、コレクション、XDoc は必要ありません( .WriteXML
/を使用.ReadXml
)。XElements を作成および整理するためのコードもすべてなくなります。
結果として得られる XML ファイルは異なりますが、それはフォーム フォローイング関数にすぎません。全部で約 60 行のコードを削除することができ、単純化されています。
資力