Custom Path of the User.Config

Define a custom path where the user.config file should be saved?

More info and a tidbit from the link which answers your question:

The "systemGUID or something" you reference is actually a hash of 2 things (Reference MSDN My.Settings):

<eid> is the URL, StrongName, or Path, based on the evidence available to hash.  
<hash> is a SHA1 hash of evidence gathered from the CurrentDomain,
in the following order of preference:
- StrongName
- URL If neither of these is available, use the .exe path.

Without a StrongName, your location is varying by path which is the problem you describe. Since BOTH eid and hash will use StrongName for the hash(es), the full path should remain the same even if they move it somewhere else or install a new version. When using a StrongName the credentials come from the app and the hashes don't change and the method of last resort (the exe path) is never used. Which answers your basic question: use a Strong Name and the path wont change.

New releases/versions will create a sub folder tree under that folder for each version for Settings. The Upgrade method for Settings mentioned in the link (apparently) facilitates importing Settings from the/a previous version. A change in the EXE Name will cause the AppDomain.FriendlyName (3rd element) to change though.


Isolated Storage is another option, and it is not as hard as it first looks, but has similar behavior. With Iso, you dont specify a folder as it just creates one in an obscure location like Users\<User>\Isolated Storage\zhxytg\dhfyres\. The location CAN remain the same for all versions of the app, even if you rename it, if you use ClickOnce (so, this is another viable solution).

I think you have to use ClickOnce (StrongName as a replacement doesnt come up in MSDN) to get Application level evidence. As a side benefit, with ISO, even under the highest security a non admin user can read/write to shared files in ProgramData\AllUsers (as might be the case for a licence, or shared settings for an app suite) at least with W7. The app's hash permits it to write to that path, so it can do some things we normally can't do.

If you dont use ClickOnce, you can still get a stable folder per install and read/write to AllUsers. A new install (to a different folder) will result in a different hash and file location; same with changing the filename. Even if you managed to store the old location somewhere, a new install probably would not have rights to the old file (havent tried).

ISO removes varying by EXEName, but it does not use My.Settings. Instead you use IsolatedFileStreams created by IsolatedStorageFile objects. And you'd have to take over organizing and managing the values and names of the various Settings. The type of Isolated Storage used (App / User) depends on the credentials available.

Isolated storage has its place, but seems to be overkill for Settings.


You mentioned that you usually only use MySettings for trivial apps. As such, a StrongName simply to stabilize the path for Settings seems to be overkill. ISO is very interesting, but there is something much simpler. This third option falls into the or other things you didn't want, but is very flexible.

Build your own Settings Class around Serialization. For simple settings, these likely arent much more than a set of Name-Value Pairs {LastPath = "....."; FormLeft = x; FormTop = y ...}. Save these in a Dictionary(Of String, String) or Dictionary(Of enumSettings, String) and just serialize (save) the entire container:

Dim bf As New BinaryFormatter
Using fs As New FileStream(myFile, FileMode.OpenOrCreate)
bf.Serialize(fs, _UserOpts)
End Using

Getting the values back is just as simple. For more complex projects where there are many Types to save like Integer, Date, Array, ArrayList, List(of T) and so forth , create a UserOptions Class for them and serialize that instead.

Note that you pass a filestream to the serializers, so you have full control over the name and location, such as C:\Users\<username>\AppData\Local\<Company>\<Product>\Settings.bin The location wont change by version, culture, assembly etc. It will stay where you put it.

This does run out of steam when you try to serilize Types like Point, Size and Font because Objects cannot be serialized directly. Especially, with ProtoBuff there are multiple options to convert these to something serializable on the fly or beforehand.

How to change the predefined userconfig directory of my .NET application?

That naming convention exists so that NET can be sure the correct settings are be loaded. Since you have yielded control of managing settings to the NET Framework/VB Application Framework, it also takes on the responsibility of making sure that an app is loading the right set of settings. That being the case, the evidence hash is used to uniquely identify one WindowsApplication1 from another (among other things).

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

It is possible, but I am not sure everything is quite the way you conclude. I very seriously doubt that many apps go thru the hassle to implement a custom provider when they could much more easily save an XML file to that location using a custom settings class.

The Simple Solution

Write your own user options class, and serialize it yourself. For instance a Shared/static method can be used to deserialize the class in very little code (this just happens to use 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

The app implementing it:

UOpt = UserOptions.Load()

Among the Pros, you have total control over where the file is saved, and can use any serializer you like. Most of all, it is simple - much less code than presented below.

The Cons are that the code using it would have to manually Load and Save them (easily handled in Application events), and there is no fancy designer for it.

The Long and Winding Road: a Custom SettingsProvider

A custom SettingsProvider will allow you to change how settings are handled, saved and loaded, including changing the folder location.

This question is narrowly focused on changing the file location. The problem is that there is not a (clean, easy) way for your app to talk to your SettingsProvider in order to specify a folder. The provider needs to be able to work that out internally and of course be consistent.

Most people will want to do more than just change the folder name used. For instance, in playing around, in place of XML I used a SQLite database which mirrored a structure the code uses. This made it very easy to load local and the correct roaming values. Were that approach taken all the way, the code could be greatly simplified and quite possibly the whole Upgrade process. So this provider takes some of those wider needs into account.

There are 2 key considerations even though you just want to change the filename:

Local vs Roaming

Coding the provider to always store in AppData\Roaming but write unqualified local settings there would be irresponsible. Distinguishing between them is a capability that ought not be sacrificed in order to elide the evidence hash in the folder name.

Note: Each Setting can be set as a Roaming or Local value: with a Setting selected in the Settings Editor, open the Properties pane - change Roaming to True.

There seems to be a consensus in the (very) few questions here dealing with a custom SettingsProvider to save Local and Roaming to the same file but in different sections. That makes a great deal of sense - and is simpler than loading from 2 files - so the XML structure used is:

<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>

The roaming items are stored in sections named after the MachineName they are used on. There might be some value in retaining the <NameSpace>.My.MySettings node, but I am not sure what purpose it serves.

I removed the SerializeAs element since it is not used.

Versions

Nothing whatsoever will happen if you invoke My.Settings.Upgrade. Even though it is a Settings method, it is actually something in ApplicationSettingsBase, so your provider isn't involved.

As a result, using the full version string as part of the folder causes a problem if you auto increment the last element. Trivial rebuilds will create a new folder and lose and orphan the old settings. Perhaps you could look for and load the values for a previous version when there is no current file. Then perhaps delete that old file/folder, so there is always only one possible set of old settings. Feel free to add oodles and oodles of merge code.

For the primary purpose of just changing the data store folder, I removed the version folder segment. When using a global provider the code automatically accumulates settings. A settings which has been removed wont "leak" into the app because NET wont ask for a value for it. The only issue is that there will be a value for it in the XML.

I added code to purge these. This will prevent a problem should you later reuse a settings name with a different type. The old saved value for Foo as Decimal wont work with the new Foo as Size for instance. Things will still go badly if you radically change a type. Don't do that.


This answer Custom path of the user.config provides a very nice starting point for a custom provider. It has a few problems and is missing a few things but provides a quick start guide for some of the steps and boilerplate code typical of any provider. Since many people may need to further modify the provider here, it might be worth reading (and upvoting).

The code here borrows a few things from that answer and:

  • Adds various refinements
  • Provides for a custom path
  • Detection for settings set as Roaming
  • Local and Roaming section in the file
  • Proper handling of complex types such as Point or Size
  • Detect and prune removed settings
  • is in VB

1. Setup

For the most part, you can't incrementally write/debug this - little will work until you are done.

  • Add a reference to System.Configuration
  • Add a new class to your project

Example:

Imports System.Configuration 
Public Class CustomSettingsProvider
Inherits SettingsProvider
End Class

Next, go to the Settings designer and add some settings for testing. Tag some as Roaming for a complete test. Then click the <> View Code button shown here:

Sample Image
Everyone loves freehand circles!

There are apparently two ways to implement a custom provider. The code here will use yours in place of My.MySettings. You can also specify a custom provider on a per-Setting basis by typing the provider name in the Properties pane, and skip the rest of this step. I did not test this, but it is how it is supposed to work.

In order to use the new settings provider "you" write, it needs to be associated with MySettings using an attribute:

Imports System.Configuration 

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

'ElektroZap' is your root NameSpace, and 'ElektroApp' is your app name, by the way. The code in the constructor can be changed to use the Product name or module name.

We are done with that file; save it and close it.

2. SettingsProvider

First, note that this CustomProvider is generic and should work with any app by just designating it as the SettingsProvider. But it really only does 2 things:

  • Uses a custom path
  • Merges local and roaming settings into one file

Typically one would have a longer ToDo list before resorting to a custom provider, so for many this may just provide the starting point for Other Things. Keep that in mind that some changes may make it specific to a project.


One of the things added is support for the more complex types such as Point or Size. These are serialized as invariant strings so that they can be parsed back. What that means is this:

Console.WriteLine(myPoint.ToString())

The result, {X=64, Y=22} cannot be directly converted back and Point lacks a Parse/TryParse method. Using the invariant string form 64,22 allows it to be converted back to the correct type. The original linked code simply used:

Convert.ChangeType(setting.DefaultValue, t);

This will work with simple types, but not Point, Font etc. I can't recall for sure, but I think this is a simple mistake of using SettingsPropertyValue.Value rather than .SerializedValue.

3. The Code

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

That's a lot of code just to elide the evidence hash from the path, but it is what MS recommends. It is also likely the only way: the property in ConfigurationManager which gets the file is read only and it backed by code.

Results:

The actual XML is as shown earlier with local/common and machine specific sections. I used several different app names and such testing various things:

Sample Image

Ignore the version portion. As noted earlier that has been removed. Otherwise the folders are correct - as noted above you have some options when it comes to the AppName segment.

Important Notes

  • The Load methods in your provider are not called unless and until the related app accesses a Settings property
  • Once loaded, the Save method will be called when the app ends (using the VB Framework) whether or not the code changes anything
  • NET seems to only save settings which are different from the default value. When using a custom provider, all the values are marked IsDirty as true and UsingDefaultValue as false.
  • If/when loaded, all the values are returned and NET simply gets values from that collection thru the life of the app

My main concern was the correct conversion of types and local/roaming support. I did not check every single possible Type. In particular, custom types and enums (I know enums will need extra handling).


It is worth noting that using a DataTable makes this much simpler. You do not need the SettingsItem class, the collection, no XDoc (use .WriteXML / .ReadXml). All the code to create and organize XElements also goes away.

The resulting XML file is different, but that is just form following function. About 60 lines of code in all can be removed, and it is just simpler.

Resources

  • SettingsProvider Class
  • MSDN blog entry about Settings
  • Stackoverflow: Custom path of the user.config "Chuck" uses better named scratch variables
  • Stackoverflow: MachineName problems in XML
  • I didnt see this CodeProject article until I was done: Creating a Custom Settings Provider This appears to be a different approach using a custom provider on a per-setting basis.

How to fix the location of user.config file for personalization

You should be able to write a custom SettingsProvider class and replace the default LocalFileSettingsProvider with this one in your setting class, e.g.:

public class MyUserSettings : ApplicationSettingsBase
{
public MyUserSettings()
: base()
{
Providers.Clear();
Providers.Add(new CustomProvider());
}
...
}

Relocating app.config file to a custom path

Each AppDomain has/can have its own configuration file. The default AppDomain created by CLR host uses programname.exe.config; if you want to provide your own configuration file, create separate AppDomain. Example:

// get the name of the assembly
string exeAssembly = Assembly.GetEntryAssembly().FullName;

// setup - there you put the path to the config file
AppDomainSetup setup = new AppDomainSetup();
setup.ApplicationBase = System.Environment.CurrentDirectory;
setup.ConfigurationFile = "<path to your config file>";

// create the app domain
AppDomain appDomain = AppDomain.CreateDomain("My AppDomain", null, setup);

// create proxy used to call the startup method
YourStartupClass proxy = (YourStartupClass)appDomain.CreateInstanceAndUnwrap(
exeAssembly, typeof(YourStartupClass).FullName);

// call the startup method - something like alternative main()
proxy.StartupMethod();

// in the end, unload the domain
AppDomain.Unload(appDomain);

Hope that helps.

How do I get the location of the user.config file in programmatically?

Try this:

var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoaming);

MessageBox.Show(config.FilePath);

How to move an 'user.config' file from source PC to target PC preserving its settings?

It would be easier for you if you have access to the code. RED2 code is available on Github here. You may download and tweak it a bit to suit your purpose.

This is not a link-only answer. As a solution to a specific problem, access to the code is the best solution in this case. I am just sharing a link to the publically available codebase for everyone to know.



Related Topics



Leave a reply



Submit