Это соглашение об именах существует для того, чтобы 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 в этом месте, используя класс пользовательских настроек.
Простое решение
Напишите свой собственный класс параметров пользователя и сериализуйте его самостоятельно. Например, метод 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()
Среди Pros у вас есть полный контроль над тем, где сохраняется файл, и вы можете использовать любой сериализатор, который вам нравится. Прежде всего, он простой — гораздо меньше кода, чем представлено ниже.
Минусы заключаются в том, что коду, использующему его, придется вручную загружать и сохранять их (это легко обрабатывается в событиях приложения), и для него нет причудливого дизайнера.
Долгая и извилистая дорога: поставщик пользовательских настроек
Пользовательский SettingsProvider
позволит вам изменить способ обработки, сохранения и загрузки настроек, включая изменение местоположения папки.
Этот вопрос узко ориентирован на изменение местоположения файла. Проблема в том, что у вашего приложения нет (чистого, простого) способа поговорить с вашим SettingsProvider
, чтобы указать папку. Поставщик должен быть в состоянии решить это внутренне и, конечно, быть последовательным.
Большинство людей захотят сделать больше, чем просто изменить имя используемой папки. Например, играя, вместо XML я использовал базу данных SQLite, которая отражала структуру, используемую кодом. Это упростило загрузку локальных и правильных значений роуминга. Если бы этот подход применялся полностью, код можно было бы значительно упростить и, вполне возможно, весь процесс обновления. Таким образом, этот провайдер учитывает некоторые из этих более широких потребностей.
Есть два ключевых момента, даже если вы просто хотите изменить имя файла:
Местные и роуминговые
Запрограммировать провайдера всегда хранить в AppData\Roaming
, но записывать туда неквалифицированные локальные настройки было бы безответственно. Различение между ними — это возможность, которой не следует жертвовать, чтобы исключить хэш доказательства в имени папки.
Примечание. Каждому Setting
можно присвоить значение Roaming
или Local
: выбрав параметр в редакторе параметров, откройте панель свойств и измените Roaming
на True.
Кажется, существует консенсус в (очень) нескольких вопросах, касающихся пользовательского SettingsProvider
для сохранения Local и Roaming в одном и том же файле, но в разных разделах. Это имеет большой смысл и проще, чем загрузка из двух файлов, поэтому используемая структура 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 обеспечивает очень хорошую отправную точку для пользовательского поставщика. У него есть несколько проблем и упущено несколько вещей, но он предоставляет краткое руководство для некоторых шагов и стандартный код, типичный для любого провайдера. Поскольку многим людям может потребоваться дальнейшее изменение провайдера здесь, возможно, стоит прочитать (и проголосовать).
Код здесь заимствует несколько вещей из этого ответа и:
- Добавляет различные уточнения
- Предоставляет пользовательский путь
- Обнаружение для настроек, установленных как Роуминг
- Раздел Local and Roaming в файле
- Правильная обработка сложных типов, таких как
Point
или Size
- Обнаружение и удаление удаленных настроек
- is in 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. Провайдер настроек
Во-первых, обратите внимание, что этот CustomProvider является универсальным и должен работать с любым приложением, просто обозначив его как SettingsProvider
. Но на самом деле он делает только 2 вещи:
- Использует пользовательский путь
- Объединяет локальные и перемещаемые настройки в один файл
Обычно перед обращением к пользовательскому поставщику требуется более длинный список дел, поэтому для многих это может стать отправной точкой для других вещей. Имейте в виду, что некоторые изменения могут сделать его специфичным для проекта.
Одна из добавленных вещей — поддержка более сложных типов, таких как 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
.
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 выглядит так, как показано ранее, с разделами local/common и для конкретных компьютеров. Я использовал несколько разных имен приложений и тестировал разные вещи:
![введите здесь описание изображения](https://i.stack.imgur.com/FmQYU.jpg)
Игнорируйте часть версии. Как отмечалось ранее, это было удалено. В противном случае папки верны - как отмечалось выше, у вас есть несколько вариантов, когда речь идет о сегменте AppName.
Важные заметки
- Методы Load в вашем провайдере не вызываются до тех пор, пока связанное приложение не получит доступ к свойству Settings.
- После загрузки метод Save будет вызываться, когда приложение завершится (с использованием VB Framework), независимо от того, меняет ли код что-либо.
- NET, по-видимому, сохраняет только те настройки, которые отличаются от значений по умолчанию. При использовании пользовательского поставщика все значения помечаются
IsDirty
как истина и UsingDefaultValue
как ложь.
- Если/при загрузке все значения возвращаются, и NET просто получает значения из этой коллекции на протяжении всего срока службы приложения.
Моя главная забота заключалась в правильном преобразовании типов и локальной/роуминговой поддержке. Я не проверял все возможные типы. В частности, пользовательские типы и перечисления (я знаю, что перечисления потребуют дополнительной обработки).
Стоит отметить, что использование DataTable
делает это намного проще. Вам не нужны ни класс SettingsItem
, ни коллекция, ни XDoc (используйте .WriteXML
/ .ReadXml
). Весь код для создания и организации XElements также исчезает.
Результирующий файл XML отличается, но это просто форма следующей функции. Всего можно удалить около 60 строк кода, и это просто проще.
Ресурсы
person
Ňɏssa Pøngjǣrdenlarp
schedule
23.07.2016
C:\Users\{User Name}\AppData\Roaming\{Assembly Name}
У меня его нет, но есть несколько, использующих такое расположение, какC:\Users\{User Name}\AppData\Roaming\CompanyName\{App Url Hash}
Большинство из них — мои приложения/апплеты. - person Ňɏssa Pøngjǣrdenlarp   schedule 20.07.2016