Visual Studio 2012 C# Common Application Data

Posted on Sunday, February 17, 2013



I am investigating the proper place to put program config files in the Windows Environment.  I am using Visual Studio 2012 as my IDE and programming in C#.  I am using InstallShield LE for my installation process.   If you are having problems getting InstallShield LE working on Visual Studio I did a little write up on it here http://www.whiteboardcoder.com/2013/02/visual-studio-pro-2012-install-shield-le.html


Where to put a program config file?


I am currently writing a Windows .Net program that when it starts will need to read some data from a config file and possibly write some data out to the same config file as well.

Looking around I found this older article http://vb.mvps.org/articles/vsm20090119.pdf [1] that suggested a good place to put this file Common AppData Folder.   This folder can differ between different Windows OS Systems, but in Windows 7 it is at C:\ProgramData  (A file that is typically hidden)





First a Default config file


First I am going to create an App.config file.




Right click on your program and then select Add --> New Item





Select General,  Then select Application Configuration File and finally click add.





You should now have an App.config file  double click on it to open it.





I updated it, adding a few keys.   Save it.



Now to the Install Shield I am going to use the InstallShield program to place a default config file in the Common AppData Folder.




Open the InstallShield Project Assistant and click on Application Files.




Right clik on Destination Computer then select Show predefined Folder and select [CommonAppDataFolder]





Right click on  CommonAppDataFolder and select New Folder




Give the folder your company name





Select the folder and click Add Files.




Find the App.config file and open it (add it to the deploy)









Right click on References and select Add Reference





Search for System.Configuration.   Checkbox System.Configuration





Now build the solution.   Click Build--> Build Solution






Run the installer that this created





Now in your Common Application Data Folder.




Update Program to read it



As a proof of concept I updated the code for a button click to the following.



        private void button1_Click(object sender, EventArgs e)
        {
            //Check to see if the Config file is in CommonApplicationData Folder
            String cadAppPath Environment.GetFolderPath(
                  Environment.SpecialFolder.
                   CommonApplicationData) + @"\Whiteboardcoder\App.Config";

            if (File.Exists(cadAppPath)){
                AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", cadAppPath);
            }

            String fName = ConfigurationManager.AppSettings["name"];
            String lName = ConfigurationManager.AppSettings["lname"];

            MessageBox.Show("Hello World " + fName + "  " + lName);
        }





I spent several hours trying to figure out how to update ConfigurationManager to read from another file.  Everything I tried or found had issues.  So instead of finding out how to switch that I found this site http://stackoverflow.com/questions/4738/using-configurationmanager-to-load-config-from-an-arbitrary-location [2]
Where a user YoYo show how to change where APP_CONFIG_DATA is.

I wrote the code to use it if it can find it else use the local one (useful when you are programming and it's not installed)





The messageBox opens up and gets data from C:\ProgramData\Whiteboardcoder\App.config file






Problem!


Well that kind of worked.  That is until I installed it then it refused to work properly.   The program would only read the App.config file in the same directory as the installed program.  

Luckily I found this post http://stackoverflow.com/questions/6150644/change-default-app-config-at-runtime [3] A forum Poster Daniel Hilgarth pointed out that there is a class called ClientConfigPaths that caches the path so updating it alone does no good.

He posted a simple class to help with the problem.  Here is the class


using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Text;

namespace HelloWorld
{
    public abstract class AppConfig : IDisposable
    {
        public static AppConfig Change(string path)
        {
            return new ChangeAppConfig(path);
        }

        public abstract void Dispose();

        private class ChangeAppConfig : AppConfig
        {
            private readonly string oldConfig =
                AppDomain.CurrentDomain.GetData("APP_CONFIG_FILE").ToString();

            private bool disposedValue;

            public ChangeAppConfig(string path)
            {
                AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path);
                ResetConfigMechanism();
            }

            public override void Dispose()
            {
                if (!disposedValue)
                {
                    AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", oldConfig);
                    ResetConfigMechanism();


                    disposedValue = true;
                }
                GC.SuppressFinalize(this);
            }

            private static void ResetConfigMechanism()
            {
                typeof(ConfigurationManager)
                    .GetField("s_initState", BindingFlags.NonPublic |
                                             BindingFlags.Static)
                    .SetValue(null, 0);

                typeof(ConfigurationManager)
                    .GetField("s_configSystem", BindingFlags.NonPublic |
                                                BindingFlags.Static)
                    .SetValue(null, null);

                typeof(ConfigurationManager)
                    .Assembly.GetTypes()
                    .Where(x => x.FullName ==
                                "System.Configuration.ClientConfigPaths")
                    .First()
                    .GetField("s_current", BindingFlags.NonPublic |
                                           BindingFlags.Static)
                    .SetValue(null, null);
            }
        }
    }
}


I added this class to my own then updated my Form class as shown below


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;


namespace HelloWorld
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //Check to see if the Config file is in              
            //CommonApplicationData Folder
            String cadAppPath = Environment.GetFolderPath(
               Environment.SpecialFolder.CommonApplicationData)
                + @"\Whiteboardcoder\App.Config";

            AppConfig.Change(cadAppPath);

            String fName = ConfigurationManager.AppSettings["name"];
            String lName = ConfigurationManager.AppSettings["lname"];

            MessageBox.Show("Hello World " + fName + "  " + lName);
        }
    }
}




Here is the important part.


Now if you make an installer for the program, install it, it will read data from C:\ProgramData\Whiteboardcoder\App.config  (well that is where it will read on my program)





Update the App.Config Data



Here is my updated code


using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;


namespace HelloWorld
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            //Check to see if the Config file is in
            //CommonApplicationData Folder
            String cadAppPath = Environment.GetFolderPath(
              Environment.SpecialFolder.CommonApplicationData)
              + @"\Whiteboardcoder\App.Config";

            AppConfig.Change(cadAppPath);

            //Now to update the data
            Configuration config = ConfigurationManager.
               OpenExeConfiguration(ConfigurationUserLevel.None);
            config.AppSettings.Settings["name"].Value = "John";
            config.AppSettings.Settings["lname"].Value = "Jones";
            config.Save(ConfigurationSaveMode.Modified);
            ConfigurationManager.RefreshSection("appSettings");


            String fName = ConfigurationManager.AppSettings["name"];
            String lName = ConfigurationManager.AppSettings["lname"];

            MessageBox.Show("Hello World " + fName + "  " + lName);
        }
    }
}




This is the portion I added





One last Problem!



I had one last problem.   I tried to install this on another Windows 7 machine using the installer I created using the InstallShield LE in Visual Studio 2012 and I got an error.




Acces to path … denied.





If I run the program as an administrator it works  (right click on .exe file and choose Run as Administrator)

This of course assumes you have admin privileges.  My user does have admin privileges so I am a bit confused


Ok I found the solution, it a UAC issue.   You need to be an admin and turn off UAC.




Open up Control Panel




Do a search for "uac"  then click on Change user Account Control Settings.






Drop it down to Never Notify and click OK.



This requires a restart of your computer.


That solved my problem.   The other solution would have been to change the privileges on the file itself so that my user could read it.

So that is a wrap.  The program is running, it installed the config file in the CommonApplicationData folder, read from that file and updated it as well.




References
[1]        Lemme Tell Ya Where to Stick it        
                Accessed 02/2013
[2]        Using ConfigurationManager to load config from an arbitrary location
                Accessed 02/2013
[3]        Change default app.config at runtime
                Accessed 02/2013
[4]        Change the value in app.config file dynamicallay
                Accessed 02/2013

2 comments:

  1. About the last problem you have there. It would also work if you just change the access level of the ProgramData\YourApp folder to full control for all users, in order to prevent an overall security setting change in your system.

    ReplyDelete