Как изменить предопределенный каталог userconfig моего приложения .NET?

c# .net vb.net user-config settingsprovider

1186 просмотра

2 ответа

8051 Репутация автора

В настоящее время пользовательские настройки моего приложения хранятся в этом каталоге по умолчанию:

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, которые могут хранить свой файл userconfig в пользовательской папке Roaming, которая не следует правилам Microsoft по умолчанию с этим необработанным хешем и другими раздражающими правилами именования.

Автор: ElektroStudios Источник Размещён: 18.07.2016 05:56

Ответы (2)


1 плюс

43021 Репутация автора

Я видел много проблем вокруг этого, например: https://stackoverflow.com/a/15726277/495455

Чтобы сделать что-то особенное, гораздо проще использовать XDoc или LinqXML со своими собственными конфигурационными файлами.

Таким образом, вы можете сохранить их там, где захотите, и не столкнетесь с другими проблемами, например: Раздел пользовательской конфигурации может быть сохранен / изменен только при работе от имени администратора?

Автор: Jeremy Thompson Размещён: 21.07.2016 10:54

6 плюса

35941 Репутация автора

Решение

Это соглашение об именах существует, так что 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()

Среди профессионалов вы имеете полный контроль над тем, где сохранен файл, и можете использовать любой сериализатор, который вам нравится. Прежде всего, это просто - гораздо меньше кода, чем представлено ниже.

В Минусы в том , что код , использующий его придется вручную загружать и сохранять их (легко обрабатываются в событиях приложений), и нет фантазии дизайнера для него.

Длинный и извилистый путь: поставщик пользовательских настроек

Пользовательский SettingsProviderрежим позволит вам изменить способ обработки, сохранения и загрузки настроек, включая изменение местоположения папки.

Этот вопрос сфокусирован на изменении местоположения файла. Проблема в том, что у вашего приложения нет (чистого, простого) способа поговорить с вами SettingsProvider, чтобы указать папку. Поставщик должен быть в состоянии решить это внутренне и, конечно, быть последовательным.

Большинство людей захотят сделать больше, чем просто изменить имя используемой папки. Например, при игре вместо XML я использовал базу данных SQLite, которая отражала структуру, которую использует код. Это позволило очень легко загружать локальные и правильные значения роуминга. Если бы этот подход использовался полностью, код мог бы быть значительно упрощен и, вполне возможно, весь процесс обновления. Таким образом, этот поставщик принимает во внимание некоторые из этих более широких потребностей.

Есть два ключевых момента, хотя вы просто хотите изменить имя файла:

Местный против Роуминга

Кодирование провайдера всегда хранить, AppData\Roamingно записывать неквалифицированные локальные настройки было бы безответственным. Различение между ними - это способность, которой не следует жертвовать, чтобы исключить хэш улик в имени папки.

Примечание. Каждое из них Settingможет быть установлено в качестве значения Roamingили Localзначения: если параметр выбран в редакторе параметров, откройте панель свойств - измените значение Roamingна True.

Кажется, есть согласие в (очень) нескольких вопросах, связанных с обычаем SettingsProviderсохранять Local и Roaming в один и тот же файл, но в разных разделах. Это имеет большой смысл - и проще, чем загрузка из 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 обеспечивает очень хорошую отправную точку для настраиваемого поставщика. Он имеет несколько проблем и пропускает несколько вещей, но предоставляет краткое руководство по началу работы для некоторых шагов и шаблонный код, типичный для любого поставщика. Поскольку многим людям здесь может понадобиться дополнительная модификация провайдера, возможно, стоит почитать (и проголосовать).

Код здесь заимствует несколько вещей из этого ответа и:

  • Добавляет различные уточнения
  • Предоставляет пользовательский путь
  • Обнаружение для настроек, установленных как Роуминг
  • Локальный и роуминг раздел в файле
  • Правильная обработка сложных типов, таких как PointилиSize
  • Обнаружение и удаление удаленных настроек
  • находится в 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. Но это действительно только 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, как показано ранее, с локальными / общими и машинно-специфическими разделами. Я использовал несколько разных названий приложений и тестировал разные вещи:

введите описание изображения здесь

Игнорировать часть версии. Как отмечалось ранее, это было удалено. В остальном папки правильные - как уже было сказано выше, у вас есть несколько вариантов, когда дело доходит до сегмента AppName.

Важные заметки

  • Методы Load в вашем провайдере не вызываются до тех пор, пока соответствующее приложение не получит доступ к свойству Settings.
  • После загрузки метод Save будет вызываться по завершении приложения (с использованием VB Framework), независимо от того, что код изменяет что-либо.
  • NET, кажется, только сохраняет настройки, которые отличаются от значения по умолчанию. При использовании настраиваемого поставщика все значения помечаются IsDirtyкак true и UsingDefaultValueкак false.
  • Если / при загрузке все значения возвращаются, и NET просто получает значения из этой коллекции в течение всего жизненного цикла приложения.

Моей главной заботой было правильное преобразование типов и поддержка локального / роуминга. Я не проверял каждый возможный тип . В частности, пользовательские типы и перечисления (я знаю, перечисления потребуют дополнительной обработки).


Стоит отметить, что использование a DataTableделает это намного проще. Вам не нужен SettingsItemкласс, коллекция, нет XDoc (используйте .WriteXML/ .ReadXml). Весь код для создания и организации XElements также исчезает.

Результирующий XML-файл отличается, но это просто форма, следующая за функцией. Всего можно удалить около 60 строк кода, и это проще.

Ресурсы

Автор: Ňɏssa Pøngjǣrdenlarp Размещён: 23.07.2016 12:58
32x32